diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 000000000..e69de29bb diff --git a/404.html b/404.html new file mode 100644 index 000000000..cf913c5fe --- /dev/null +++ b/404.html @@ -0,0 +1,2577 @@ + + + + + + + + + + + + + + + + + + PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ +

404 - Not found

+ +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/_theme/main.html b/_theme/main.html new file mode 100644 index 000000000..7d32bb1e1 --- /dev/null +++ b/_theme/main.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block analytics %} + spacer +{% endblock %} diff --git a/assets/images/favicon.png b/assets/images/favicon.png new file mode 100644 index 000000000..1cf13b9f9 Binary files /dev/null and b/assets/images/favicon.png differ diff --git a/assets/javascripts/bundle.51d95adb.min.js b/assets/javascripts/bundle.51d95adb.min.js new file mode 100644 index 000000000..b20ec6835 --- /dev/null +++ b/assets/javascripts/bundle.51d95adb.min.js @@ -0,0 +1,29 @@ +"use strict";(()=>{var Hi=Object.create;var xr=Object.defineProperty;var Pi=Object.getOwnPropertyDescriptor;var $i=Object.getOwnPropertyNames,kt=Object.getOwnPropertySymbols,Ii=Object.getPrototypeOf,Er=Object.prototype.hasOwnProperty,an=Object.prototype.propertyIsEnumerable;var on=(e,t,r)=>t in e?xr(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,P=(e,t)=>{for(var r in t||(t={}))Er.call(t,r)&&on(e,r,t[r]);if(kt)for(var r of kt(t))an.call(t,r)&&on(e,r,t[r]);return e};var sn=(e,t)=>{var r={};for(var n in e)Er.call(e,n)&&t.indexOf(n)<0&&(r[n]=e[n]);if(e!=null&&kt)for(var n of kt(e))t.indexOf(n)<0&&an.call(e,n)&&(r[n]=e[n]);return r};var Ht=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Fi=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of $i(t))!Er.call(e,o)&&o!==r&&xr(e,o,{get:()=>t[o],enumerable:!(n=Pi(t,o))||n.enumerable});return e};var yt=(e,t,r)=>(r=e!=null?Hi(Ii(e)):{},Fi(t||!e||!e.__esModule?xr(r,"default",{value:e,enumerable:!0}):r,e));var fn=Ht((wr,cn)=>{(function(e,t){typeof wr=="object"&&typeof cn!="undefined"?t():typeof define=="function"&&define.amd?define(t):t()})(wr,function(){"use strict";function e(r){var n=!0,o=!1,i=null,a={text:!0,search:!0,url:!0,tel:!0,email:!0,password:!0,number:!0,date:!0,month:!0,week:!0,time:!0,datetime:!0,"datetime-local":!0};function s(T){return!!(T&&T!==document&&T.nodeName!=="HTML"&&T.nodeName!=="BODY"&&"classList"in T&&"contains"in T.classList)}function f(T){var Ke=T.type,We=T.tagName;return!!(We==="INPUT"&&a[Ke]&&!T.readOnly||We==="TEXTAREA"&&!T.readOnly||T.isContentEditable)}function c(T){T.classList.contains("focus-visible")||(T.classList.add("focus-visible"),T.setAttribute("data-focus-visible-added",""))}function u(T){T.hasAttribute("data-focus-visible-added")&&(T.classList.remove("focus-visible"),T.removeAttribute("data-focus-visible-added"))}function p(T){T.metaKey||T.altKey||T.ctrlKey||(s(r.activeElement)&&c(r.activeElement),n=!0)}function m(T){n=!1}function d(T){s(T.target)&&(n||f(T.target))&&c(T.target)}function h(T){s(T.target)&&(T.target.classList.contains("focus-visible")||T.target.hasAttribute("data-focus-visible-added"))&&(o=!0,window.clearTimeout(i),i=window.setTimeout(function(){o=!1},100),u(T.target))}function v(T){document.visibilityState==="hidden"&&(o&&(n=!0),B())}function B(){document.addEventListener("mousemove",z),document.addEventListener("mousedown",z),document.addEventListener("mouseup",z),document.addEventListener("pointermove",z),document.addEventListener("pointerdown",z),document.addEventListener("pointerup",z),document.addEventListener("touchmove",z),document.addEventListener("touchstart",z),document.addEventListener("touchend",z)}function re(){document.removeEventListener("mousemove",z),document.removeEventListener("mousedown",z),document.removeEventListener("mouseup",z),document.removeEventListener("pointermove",z),document.removeEventListener("pointerdown",z),document.removeEventListener("pointerup",z),document.removeEventListener("touchmove",z),document.removeEventListener("touchstart",z),document.removeEventListener("touchend",z)}function z(T){T.target.nodeName&&T.target.nodeName.toLowerCase()==="html"||(n=!1,re())}document.addEventListener("keydown",p,!0),document.addEventListener("mousedown",m,!0),document.addEventListener("pointerdown",m,!0),document.addEventListener("touchstart",m,!0),document.addEventListener("visibilitychange",v,!0),B(),r.addEventListener("focus",d,!0),r.addEventListener("blur",h,!0),r.nodeType===Node.DOCUMENT_FRAGMENT_NODE&&r.host?r.host.setAttribute("data-js-focus-visible",""):r.nodeType===Node.DOCUMENT_NODE&&(document.documentElement.classList.add("js-focus-visible"),document.documentElement.setAttribute("data-js-focus-visible",""))}if(typeof window!="undefined"&&typeof document!="undefined"){window.applyFocusVisiblePolyfill=e;var t;try{t=new CustomEvent("focus-visible-polyfill-ready")}catch(r){t=document.createEvent("CustomEvent"),t.initCustomEvent("focus-visible-polyfill-ready",!1,!1,{})}window.dispatchEvent(t)}typeof document!="undefined"&&e(document)})});var un=Ht(Sr=>{(function(e){var t=function(){try{return!!Symbol.iterator}catch(c){return!1}},r=t(),n=function(c){var u={next:function(){var p=c.shift();return{done:p===void 0,value:p}}};return r&&(u[Symbol.iterator]=function(){return u}),u},o=function(c){return encodeURIComponent(c).replace(/%20/g,"+")},i=function(c){return decodeURIComponent(String(c).replace(/\+/g," "))},a=function(){var c=function(p){Object.defineProperty(this,"_entries",{writable:!0,value:{}});var m=typeof p;if(m!=="undefined")if(m==="string")p!==""&&this._fromString(p);else if(p instanceof c){var d=this;p.forEach(function(re,z){d.append(z,re)})}else if(p!==null&&m==="object")if(Object.prototype.toString.call(p)==="[object Array]")for(var h=0;hd[0]?1:0}),c._entries&&(c._entries={});for(var p=0;p1?i(d[1]):"")}})})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr);(function(e){var t=function(){try{var o=new e.URL("b","http://a");return o.pathname="c d",o.href==="http://a/c%20d"&&o.searchParams}catch(i){return!1}},r=function(){var o=e.URL,i=function(f,c){typeof f!="string"&&(f=String(f)),c&&typeof c!="string"&&(c=String(c));var u=document,p;if(c&&(e.location===void 0||c!==e.location.href)){c=c.toLowerCase(),u=document.implementation.createHTMLDocument(""),p=u.createElement("base"),p.href=c,u.head.appendChild(p);try{if(p.href.indexOf(c)!==0)throw new Error(p.href)}catch(T){throw new Error("URL unable to set base "+c+" due to "+T)}}var m=u.createElement("a");m.href=f,p&&(u.body.appendChild(m),m.href=m.href);var d=u.createElement("input");if(d.type="url",d.value=f,m.protocol===":"||!/:/.test(m.href)||!d.checkValidity()&&!c)throw new TypeError("Invalid URL");Object.defineProperty(this,"_anchorElement",{value:m});var h=new e.URLSearchParams(this.search),v=!0,B=!0,re=this;["append","delete","set"].forEach(function(T){var Ke=h[T];h[T]=function(){Ke.apply(h,arguments),v&&(B=!1,re.search=h.toString(),B=!0)}}),Object.defineProperty(this,"searchParams",{value:h,enumerable:!0});var z=void 0;Object.defineProperty(this,"_updateSearchParams",{enumerable:!1,configurable:!1,writable:!1,value:function(){this.search!==z&&(z=this.search,B&&(v=!1,this.searchParams._fromString(this.search),v=!0))}})},a=i.prototype,s=function(f){Object.defineProperty(a,f,{get:function(){return this._anchorElement[f]},set:function(c){this._anchorElement[f]=c},enumerable:!0})};["hash","host","hostname","port","protocol"].forEach(function(f){s(f)}),Object.defineProperty(a,"search",{get:function(){return this._anchorElement.search},set:function(f){this._anchorElement.search=f,this._updateSearchParams()},enumerable:!0}),Object.defineProperties(a,{toString:{get:function(){var f=this;return function(){return f.href}}},href:{get:function(){return this._anchorElement.href.replace(/\?$/,"")},set:function(f){this._anchorElement.href=f,this._updateSearchParams()},enumerable:!0},pathname:{get:function(){return this._anchorElement.pathname.replace(/(^\/?)/,"/")},set:function(f){this._anchorElement.pathname=f},enumerable:!0},origin:{get:function(){var f={"http:":80,"https:":443,"ftp:":21}[this._anchorElement.protocol],c=this._anchorElement.port!=f&&this._anchorElement.port!=="";return this._anchorElement.protocol+"//"+this._anchorElement.hostname+(c?":"+this._anchorElement.port:"")},enumerable:!0},password:{get:function(){return""},set:function(f){},enumerable:!0},username:{get:function(){return""},set:function(f){},enumerable:!0}}),i.createObjectURL=function(f){return o.createObjectURL.apply(o,arguments)},i.revokeObjectURL=function(f){return o.revokeObjectURL.apply(o,arguments)},e.URL=i};if(t()||r(),e.location!==void 0&&!("origin"in e.location)){var n=function(){return e.location.protocol+"//"+e.location.hostname+(e.location.port?":"+e.location.port:"")};try{Object.defineProperty(e.location,"origin",{get:n,enumerable:!0})}catch(o){setInterval(function(){e.location.origin=n()},100)}}})(typeof global!="undefined"?global:typeof window!="undefined"?window:typeof self!="undefined"?self:Sr)});var Qr=Ht((Lt,Kr)=>{/*! + * clipboard.js v2.0.11 + * https://clipboardjs.com/ + * + * Licensed MIT © Zeno Rocha + */(function(t,r){typeof Lt=="object"&&typeof Kr=="object"?Kr.exports=r():typeof define=="function"&&define.amd?define([],r):typeof Lt=="object"?Lt.ClipboardJS=r():t.ClipboardJS=r()})(Lt,function(){return function(){var e={686:function(n,o,i){"use strict";i.d(o,{default:function(){return ki}});var a=i(279),s=i.n(a),f=i(370),c=i.n(f),u=i(817),p=i.n(u);function m(j){try{return document.execCommand(j)}catch(O){return!1}}var d=function(O){var w=p()(O);return m("cut"),w},h=d;function v(j){var O=document.documentElement.getAttribute("dir")==="rtl",w=document.createElement("textarea");w.style.fontSize="12pt",w.style.border="0",w.style.padding="0",w.style.margin="0",w.style.position="absolute",w.style[O?"right":"left"]="-9999px";var k=window.pageYOffset||document.documentElement.scrollTop;return w.style.top="".concat(k,"px"),w.setAttribute("readonly",""),w.value=j,w}var B=function(O,w){var k=v(O);w.container.appendChild(k);var F=p()(k);return m("copy"),k.remove(),F},re=function(O){var w=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body},k="";return typeof O=="string"?k=B(O,w):O instanceof HTMLInputElement&&!["text","search","url","tel","password"].includes(O==null?void 0:O.type)?k=B(O.value,w):(k=p()(O),m("copy")),k},z=re;function T(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?T=function(w){return typeof w}:T=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},T(j)}var Ke=function(){var O=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{},w=O.action,k=w===void 0?"copy":w,F=O.container,q=O.target,Le=O.text;if(k!=="copy"&&k!=="cut")throw new Error('Invalid "action" value, use either "copy" or "cut"');if(q!==void 0)if(q&&T(q)==="object"&&q.nodeType===1){if(k==="copy"&&q.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if(k==="cut"&&(q.hasAttribute("readonly")||q.hasAttribute("disabled")))throw new Error(`Invalid "target" attribute. You can't cut text from elements with "readonly" or "disabled" attributes`)}else throw new Error('Invalid "target" value, use a valid Element');if(Le)return z(Le,{container:F});if(q)return k==="cut"?h(q):z(q,{container:F})},We=Ke;function Ie(j){return typeof Symbol=="function"&&typeof Symbol.iterator=="symbol"?Ie=function(w){return typeof w}:Ie=function(w){return w&&typeof Symbol=="function"&&w.constructor===Symbol&&w!==Symbol.prototype?"symbol":typeof w},Ie(j)}function Ti(j,O){if(!(j instanceof O))throw new TypeError("Cannot call a class as a function")}function nn(j,O){for(var w=0;w0&&arguments[0]!==void 0?arguments[0]:{};this.action=typeof F.action=="function"?F.action:this.defaultAction,this.target=typeof F.target=="function"?F.target:this.defaultTarget,this.text=typeof F.text=="function"?F.text:this.defaultText,this.container=Ie(F.container)==="object"?F.container:document.body}},{key:"listenClick",value:function(F){var q=this;this.listener=c()(F,"click",function(Le){return q.onClick(Le)})}},{key:"onClick",value:function(F){var q=F.delegateTarget||F.currentTarget,Le=this.action(q)||"copy",Rt=We({action:Le,container:this.container,target:this.target(q),text:this.text(q)});this.emit(Rt?"success":"error",{action:Le,text:Rt,trigger:q,clearSelection:function(){q&&q.focus(),window.getSelection().removeAllRanges()}})}},{key:"defaultAction",value:function(F){return yr("action",F)}},{key:"defaultTarget",value:function(F){var q=yr("target",F);if(q)return document.querySelector(q)}},{key:"defaultText",value:function(F){return yr("text",F)}},{key:"destroy",value:function(){this.listener.destroy()}}],[{key:"copy",value:function(F){var q=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{container:document.body};return z(F,q)}},{key:"cut",value:function(F){return h(F)}},{key:"isSupported",value:function(){var F=arguments.length>0&&arguments[0]!==void 0?arguments[0]:["copy","cut"],q=typeof F=="string"?[F]:F,Le=!!document.queryCommandSupported;return q.forEach(function(Rt){Le=Le&&!!document.queryCommandSupported(Rt)}),Le}}]),w}(s()),ki=Ri},828:function(n){var o=9;if(typeof Element!="undefined"&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}function a(s,f){for(;s&&s.nodeType!==o;){if(typeof s.matches=="function"&&s.matches(f))return s;s=s.parentNode}}n.exports=a},438:function(n,o,i){var a=i(828);function s(u,p,m,d,h){var v=c.apply(this,arguments);return u.addEventListener(m,v,h),{destroy:function(){u.removeEventListener(m,v,h)}}}function f(u,p,m,d,h){return typeof u.addEventListener=="function"?s.apply(null,arguments):typeof m=="function"?s.bind(null,document).apply(null,arguments):(typeof u=="string"&&(u=document.querySelectorAll(u)),Array.prototype.map.call(u,function(v){return s(v,p,m,d,h)}))}function c(u,p,m,d){return function(h){h.delegateTarget=a(h.target,p),h.delegateTarget&&d.call(u,h)}}n.exports=f},879:function(n,o){o.node=function(i){return i!==void 0&&i instanceof HTMLElement&&i.nodeType===1},o.nodeList=function(i){var a=Object.prototype.toString.call(i);return i!==void 0&&(a==="[object NodeList]"||a==="[object HTMLCollection]")&&"length"in i&&(i.length===0||o.node(i[0]))},o.string=function(i){return typeof i=="string"||i instanceof String},o.fn=function(i){var a=Object.prototype.toString.call(i);return a==="[object Function]"}},370:function(n,o,i){var a=i(879),s=i(438);function f(m,d,h){if(!m&&!d&&!h)throw new Error("Missing required arguments");if(!a.string(d))throw new TypeError("Second argument must be a String");if(!a.fn(h))throw new TypeError("Third argument must be a Function");if(a.node(m))return c(m,d,h);if(a.nodeList(m))return u(m,d,h);if(a.string(m))return p(m,d,h);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function c(m,d,h){return m.addEventListener(d,h),{destroy:function(){m.removeEventListener(d,h)}}}function u(m,d,h){return Array.prototype.forEach.call(m,function(v){v.addEventListener(d,h)}),{destroy:function(){Array.prototype.forEach.call(m,function(v){v.removeEventListener(d,h)})}}}function p(m,d,h){return s(document.body,m,d,h)}n.exports=f},817:function(n){function o(i){var a;if(i.nodeName==="SELECT")i.focus(),a=i.value;else if(i.nodeName==="INPUT"||i.nodeName==="TEXTAREA"){var s=i.hasAttribute("readonly");s||i.setAttribute("readonly",""),i.select(),i.setSelectionRange(0,i.value.length),s||i.removeAttribute("readonly"),a=i.value}else{i.hasAttribute("contenteditable")&&i.focus();var f=window.getSelection(),c=document.createRange();c.selectNodeContents(i),f.removeAllRanges(),f.addRange(c),a=f.toString()}return a}n.exports=o},279:function(n){function o(){}o.prototype={on:function(i,a,s){var f=this.e||(this.e={});return(f[i]||(f[i]=[])).push({fn:a,ctx:s}),this},once:function(i,a,s){var f=this;function c(){f.off(i,c),a.apply(s,arguments)}return c._=a,this.on(i,c,s)},emit:function(i){var a=[].slice.call(arguments,1),s=((this.e||(this.e={}))[i]||[]).slice(),f=0,c=s.length;for(f;f{"use strict";/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var is=/["'&<>]/;Jo.exports=as;function as(e){var t=""+e,r=is.exec(t);if(!r)return t;var n,o="",i=0,a=0;for(i=r.index;i0&&i[i.length-1])&&(c[0]===6||c[0]===2)){r=0;continue}if(c[0]===3&&(!i||c[1]>i[0]&&c[1]=e.length&&(e=void 0),{value:e&&e[n++],done:!e}}};throw new TypeError(t?"Object is not iterable.":"Symbol.iterator is not defined.")}function W(e,t){var r=typeof Symbol=="function"&&e[Symbol.iterator];if(!r)return e;var n=r.call(e),o,i=[],a;try{for(;(t===void 0||t-- >0)&&!(o=n.next()).done;)i.push(o.value)}catch(s){a={error:s}}finally{try{o&&!o.done&&(r=n.return)&&r.call(n)}finally{if(a)throw a.error}}return i}function D(e,t,r){if(r||arguments.length===2)for(var n=0,o=t.length,i;n1||s(m,d)})})}function s(m,d){try{f(n[m](d))}catch(h){p(i[0][3],h)}}function f(m){m.value instanceof Xe?Promise.resolve(m.value.v).then(c,u):p(i[0][2],m)}function c(m){s("next",m)}function u(m){s("throw",m)}function p(m,d){m(d),i.shift(),i.length&&s(i[0][0],i[0][1])}}function mn(e){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var t=e[Symbol.asyncIterator],r;return t?t.call(e):(e=typeof xe=="function"?xe(e):e[Symbol.iterator](),r={},n("next"),n("throw"),n("return"),r[Symbol.asyncIterator]=function(){return this},r);function n(i){r[i]=e[i]&&function(a){return new Promise(function(s,f){a=e[i](a),o(s,f,a.done,a.value)})}}function o(i,a,s,f){Promise.resolve(f).then(function(c){i({value:c,done:s})},a)}}function A(e){return typeof e=="function"}function at(e){var t=function(n){Error.call(n),n.stack=new Error().stack},r=e(t);return r.prototype=Object.create(Error.prototype),r.prototype.constructor=r,r}var $t=at(function(e){return function(r){e(this),this.message=r?r.length+` errors occurred during unsubscription: +`+r.map(function(n,o){return o+1+") "+n.toString()}).join(` + `):"",this.name="UnsubscriptionError",this.errors=r}});function De(e,t){if(e){var r=e.indexOf(t);0<=r&&e.splice(r,1)}}var Fe=function(){function e(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._finalizers=null}return e.prototype.unsubscribe=function(){var t,r,n,o,i;if(!this.closed){this.closed=!0;var a=this._parentage;if(a)if(this._parentage=null,Array.isArray(a))try{for(var s=xe(a),f=s.next();!f.done;f=s.next()){var c=f.value;c.remove(this)}}catch(v){t={error:v}}finally{try{f&&!f.done&&(r=s.return)&&r.call(s)}finally{if(t)throw t.error}}else a.remove(this);var u=this.initialTeardown;if(A(u))try{u()}catch(v){i=v instanceof $t?v.errors:[v]}var p=this._finalizers;if(p){this._finalizers=null;try{for(var m=xe(p),d=m.next();!d.done;d=m.next()){var h=d.value;try{dn(h)}catch(v){i=i!=null?i:[],v instanceof $t?i=D(D([],W(i)),W(v.errors)):i.push(v)}}}catch(v){n={error:v}}finally{try{d&&!d.done&&(o=m.return)&&o.call(m)}finally{if(n)throw n.error}}}if(i)throw new $t(i)}},e.prototype.add=function(t){var r;if(t&&t!==this)if(this.closed)dn(t);else{if(t instanceof e){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._finalizers=(r=this._finalizers)!==null&&r!==void 0?r:[]).push(t)}},e.prototype._hasParent=function(t){var r=this._parentage;return r===t||Array.isArray(r)&&r.includes(t)},e.prototype._addParent=function(t){var r=this._parentage;this._parentage=Array.isArray(r)?(r.push(t),r):r?[r,t]:t},e.prototype._removeParent=function(t){var r=this._parentage;r===t?this._parentage=null:Array.isArray(r)&&De(r,t)},e.prototype.remove=function(t){var r=this._finalizers;r&&De(r,t),t instanceof e&&t._removeParent(this)},e.EMPTY=function(){var t=new e;return t.closed=!0,t}(),e}();var Or=Fe.EMPTY;function It(e){return e instanceof Fe||e&&"closed"in e&&A(e.remove)&&A(e.add)&&A(e.unsubscribe)}function dn(e){A(e)?e():e.unsubscribe()}var Ae={onUnhandledError:null,onStoppedNotification:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1};var st={setTimeout:function(e,t){for(var r=[],n=2;n0},enumerable:!1,configurable:!0}),t.prototype._trySubscribe=function(r){return this._throwIfClosed(),e.prototype._trySubscribe.call(this,r)},t.prototype._subscribe=function(r){return this._throwIfClosed(),this._checkFinalizedStatuses(r),this._innerSubscribe(r)},t.prototype._innerSubscribe=function(r){var n=this,o=this,i=o.hasError,a=o.isStopped,s=o.observers;return i||a?Or:(this.currentObservers=null,s.push(r),new Fe(function(){n.currentObservers=null,De(s,r)}))},t.prototype._checkFinalizedStatuses=function(r){var n=this,o=n.hasError,i=n.thrownError,a=n.isStopped;o?r.error(i):a&&r.complete()},t.prototype.asObservable=function(){var r=new U;return r.source=this,r},t.create=function(r,n){return new wn(r,n)},t}(U);var wn=function(e){ne(t,e);function t(r,n){var o=e.call(this)||this;return o.destination=r,o.source=n,o}return t.prototype.next=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.next)===null||o===void 0||o.call(n,r)},t.prototype.error=function(r){var n,o;(o=(n=this.destination)===null||n===void 0?void 0:n.error)===null||o===void 0||o.call(n,r)},t.prototype.complete=function(){var r,n;(n=(r=this.destination)===null||r===void 0?void 0:r.complete)===null||n===void 0||n.call(r)},t.prototype._subscribe=function(r){var n,o;return(o=(n=this.source)===null||n===void 0?void 0:n.subscribe(r))!==null&&o!==void 0?o:Or},t}(E);var Et={now:function(){return(Et.delegate||Date).now()},delegate:void 0};var wt=function(e){ne(t,e);function t(r,n,o){r===void 0&&(r=1/0),n===void 0&&(n=1/0),o===void 0&&(o=Et);var i=e.call(this)||this;return i._bufferSize=r,i._windowTime=n,i._timestampProvider=o,i._buffer=[],i._infiniteTimeWindow=!0,i._infiniteTimeWindow=n===1/0,i._bufferSize=Math.max(1,r),i._windowTime=Math.max(1,n),i}return t.prototype.next=function(r){var n=this,o=n.isStopped,i=n._buffer,a=n._infiniteTimeWindow,s=n._timestampProvider,f=n._windowTime;o||(i.push(r),!a&&i.push(s.now()+f)),this._trimBuffer(),e.prototype.next.call(this,r)},t.prototype._subscribe=function(r){this._throwIfClosed(),this._trimBuffer();for(var n=this._innerSubscribe(r),o=this,i=o._infiniteTimeWindow,a=o._buffer,s=a.slice(),f=0;f0?e.prototype.requestAsyncId.call(this,r,n,o):(r.actions.push(this),r._scheduled||(r._scheduled=ut.requestAnimationFrame(function(){return r.flush(void 0)})))},t.prototype.recycleAsyncId=function(r,n,o){var i;if(o===void 0&&(o=0),o!=null?o>0:this.delay>0)return e.prototype.recycleAsyncId.call(this,r,n,o);var a=r.actions;n!=null&&((i=a[a.length-1])===null||i===void 0?void 0:i.id)!==n&&(ut.cancelAnimationFrame(n),r._scheduled=void 0)},t}(Ut);var On=function(e){ne(t,e);function t(){return e!==null&&e.apply(this,arguments)||this}return t.prototype.flush=function(r){this._active=!0;var n=this._scheduled;this._scheduled=void 0;var o=this.actions,i;r=r||o.shift();do if(i=r.execute(r.state,r.delay))break;while((r=o[0])&&r.id===n&&o.shift());if(this._active=!1,i){for(;(r=o[0])&&r.id===n&&o.shift();)r.unsubscribe();throw i}},t}(Wt);var we=new On(Tn);var R=new U(function(e){return e.complete()});function Dt(e){return e&&A(e.schedule)}function kr(e){return e[e.length-1]}function Qe(e){return A(kr(e))?e.pop():void 0}function Se(e){return Dt(kr(e))?e.pop():void 0}function Vt(e,t){return typeof kr(e)=="number"?e.pop():t}var pt=function(e){return e&&typeof e.length=="number"&&typeof e!="function"};function zt(e){return A(e==null?void 0:e.then)}function Nt(e){return A(e[ft])}function qt(e){return Symbol.asyncIterator&&A(e==null?void 0:e[Symbol.asyncIterator])}function Kt(e){return new TypeError("You provided "+(e!==null&&typeof e=="object"?"an invalid object":"'"+e+"'")+" where a stream was expected. You can provide an Observable, Promise, ReadableStream, Array, AsyncIterable, or Iterable.")}function Ki(){return typeof Symbol!="function"||!Symbol.iterator?"@@iterator":Symbol.iterator}var Qt=Ki();function Yt(e){return A(e==null?void 0:e[Qt])}function Gt(e){return ln(this,arguments,function(){var r,n,o,i;return Pt(this,function(a){switch(a.label){case 0:r=e.getReader(),a.label=1;case 1:a.trys.push([1,,9,10]),a.label=2;case 2:return[4,Xe(r.read())];case 3:return n=a.sent(),o=n.value,i=n.done,i?[4,Xe(void 0)]:[3,5];case 4:return[2,a.sent()];case 5:return[4,Xe(o)];case 6:return[4,a.sent()];case 7:return a.sent(),[3,2];case 8:return[3,10];case 9:return r.releaseLock(),[7];case 10:return[2]}})})}function Bt(e){return A(e==null?void 0:e.getReader)}function $(e){if(e instanceof U)return e;if(e!=null){if(Nt(e))return Qi(e);if(pt(e))return Yi(e);if(zt(e))return Gi(e);if(qt(e))return _n(e);if(Yt(e))return Bi(e);if(Bt(e))return Ji(e)}throw Kt(e)}function Qi(e){return new U(function(t){var r=e[ft]();if(A(r.subscribe))return r.subscribe(t);throw new TypeError("Provided object does not correctly implement Symbol.observable")})}function Yi(e){return new U(function(t){for(var r=0;r=2;return function(n){return n.pipe(e?_(function(o,i){return e(o,i,n)}):me,Oe(1),r?He(t):zn(function(){return new Xt}))}}function Nn(){for(var e=[],t=0;t=2,!0))}function fe(e){e===void 0&&(e={});var t=e.connector,r=t===void 0?function(){return new E}:t,n=e.resetOnError,o=n===void 0?!0:n,i=e.resetOnComplete,a=i===void 0?!0:i,s=e.resetOnRefCountZero,f=s===void 0?!0:s;return function(c){var u,p,m,d=0,h=!1,v=!1,B=function(){p==null||p.unsubscribe(),p=void 0},re=function(){B(),u=m=void 0,h=v=!1},z=function(){var T=u;re(),T==null||T.unsubscribe()};return g(function(T,Ke){d++,!v&&!h&&B();var We=m=m!=null?m:r();Ke.add(function(){d--,d===0&&!v&&!h&&(p=jr(z,f))}),We.subscribe(Ke),!u&&d>0&&(u=new et({next:function(Ie){return We.next(Ie)},error:function(Ie){v=!0,B(),p=jr(re,o,Ie),We.error(Ie)},complete:function(){h=!0,B(),p=jr(re,a),We.complete()}}),$(T).subscribe(u))})(c)}}function jr(e,t){for(var r=[],n=2;ne.next(document)),e}function K(e,t=document){return Array.from(t.querySelectorAll(e))}function V(e,t=document){let r=se(e,t);if(typeof r=="undefined")throw new ReferenceError(`Missing element: expected "${e}" to be present`);return r}function se(e,t=document){return t.querySelector(e)||void 0}function _e(){return document.activeElement instanceof HTMLElement&&document.activeElement||void 0}function tr(e){return L(b(document.body,"focusin"),b(document.body,"focusout")).pipe(ke(1),l(()=>{let t=_e();return typeof t!="undefined"?e.contains(t):!1}),N(e===_e()),Y())}function Be(e){return{x:e.offsetLeft,y:e.offsetTop}}function Yn(e){return L(b(window,"load"),b(window,"resize")).pipe(Ce(0,we),l(()=>Be(e)),N(Be(e)))}function rr(e){return{x:e.scrollLeft,y:e.scrollTop}}function dt(e){return L(b(e,"scroll"),b(window,"resize")).pipe(Ce(0,we),l(()=>rr(e)),N(rr(e)))}var Bn=function(){if(typeof Map!="undefined")return Map;function e(t,r){var n=-1;return t.some(function(o,i){return o[0]===r?(n=i,!0):!1}),n}return function(){function t(){this.__entries__=[]}return Object.defineProperty(t.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),t.prototype.get=function(r){var n=e(this.__entries__,r),o=this.__entries__[n];return o&&o[1]},t.prototype.set=function(r,n){var o=e(this.__entries__,r);~o?this.__entries__[o][1]=n:this.__entries__.push([r,n])},t.prototype.delete=function(r){var n=this.__entries__,o=e(n,r);~o&&n.splice(o,1)},t.prototype.has=function(r){return!!~e(this.__entries__,r)},t.prototype.clear=function(){this.__entries__.splice(0)},t.prototype.forEach=function(r,n){n===void 0&&(n=null);for(var o=0,i=this.__entries__;o0},e.prototype.connect_=function(){!zr||this.connected_||(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),xa?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},e.prototype.disconnect_=function(){!zr||!this.connected_||(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},e.prototype.onTransitionEnd_=function(t){var r=t.propertyName,n=r===void 0?"":r,o=ya.some(function(i){return!!~n.indexOf(i)});o&&this.refresh()},e.getInstance=function(){return this.instance_||(this.instance_=new e),this.instance_},e.instance_=null,e}(),Jn=function(e,t){for(var r=0,n=Object.keys(t);r0},e}(),Zn=typeof WeakMap!="undefined"?new WeakMap:new Bn,eo=function(){function e(t){if(!(this instanceof e))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var r=Ea.getInstance(),n=new Ra(t,r,this);Zn.set(this,n)}return e}();["observe","unobserve","disconnect"].forEach(function(e){eo.prototype[e]=function(){var t;return(t=Zn.get(this))[e].apply(t,arguments)}});var ka=function(){return typeof nr.ResizeObserver!="undefined"?nr.ResizeObserver:eo}(),to=ka;var ro=new E,Ha=I(()=>H(new to(e=>{for(let t of e)ro.next(t)}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function de(e){return{width:e.offsetWidth,height:e.offsetHeight}}function ge(e){return Ha.pipe(S(t=>t.observe(e)),x(t=>ro.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(()=>de(e)))),N(de(e)))}function bt(e){return{width:e.scrollWidth,height:e.scrollHeight}}function ar(e){let t=e.parentElement;for(;t&&(e.scrollWidth<=t.scrollWidth&&e.scrollHeight<=t.scrollHeight);)t=(e=t).parentElement;return t?e:void 0}var no=new E,Pa=I(()=>H(new IntersectionObserver(e=>{for(let t of e)no.next(t)},{threshold:0}))).pipe(x(e=>L(Te,H(e)).pipe(C(()=>e.disconnect()))),J(1));function sr(e){return Pa.pipe(S(t=>t.observe(e)),x(t=>no.pipe(_(({target:r})=>r===e),C(()=>t.unobserve(e)),l(({isIntersecting:r})=>r))))}function oo(e,t=16){return dt(e).pipe(l(({y:r})=>{let n=de(e),o=bt(e);return r>=o.height-n.height-t}),Y())}var cr={drawer:V("[data-md-toggle=drawer]"),search:V("[data-md-toggle=search]")};function io(e){return cr[e].checked}function qe(e,t){cr[e].checked!==t&&cr[e].click()}function je(e){let t=cr[e];return b(t,"change").pipe(l(()=>t.checked),N(t.checked))}function $a(e,t){switch(e.constructor){case HTMLInputElement:return e.type==="radio"?/^Arrow/.test(t):!0;case HTMLSelectElement:case HTMLTextAreaElement:return!0;default:return e.isContentEditable}}function Ia(){return L(b(window,"compositionstart").pipe(l(()=>!0)),b(window,"compositionend").pipe(l(()=>!1))).pipe(N(!1))}function ao(){let e=b(window,"keydown").pipe(_(t=>!(t.metaKey||t.ctrlKey)),l(t=>({mode:io("search")?"search":"global",type:t.key,claim(){t.preventDefault(),t.stopPropagation()}})),_(({mode:t,type:r})=>{if(t==="global"){let n=_e();if(typeof n!="undefined")return!$a(n,r)}return!0}),fe());return Ia().pipe(x(t=>t?R:e))}function Me(){return new URL(location.href)}function ot(e){location.href=e.href}function so(){return new E}function co(e,t){if(typeof t=="string"||typeof t=="number")e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(let r of t)co(e,r)}function M(e,t,...r){let n=document.createElement(e);if(t)for(let o of Object.keys(t))typeof t[o]!="undefined"&&(typeof t[o]!="boolean"?n.setAttribute(o,t[o]):n.setAttribute(o,""));for(let o of r)co(n,o);return n}function fr(e){if(e>999){let t=+((e-950)%1e3>99);return`${((e+1e-6)/1e3).toFixed(t)}k`}else return e.toString()}function fo(){return location.hash.substring(1)}function uo(e){let t=M("a",{href:e});t.addEventListener("click",r=>r.stopPropagation()),t.click()}function Fa(){return b(window,"hashchange").pipe(l(fo),N(fo()),_(e=>e.length>0),J(1))}function po(){return Fa().pipe(l(e=>se(`[id="${e}"]`)),_(e=>typeof e!="undefined"))}function Nr(e){let t=matchMedia(e);return Zt(r=>t.addListener(()=>r(t.matches))).pipe(N(t.matches))}function lo(){let e=matchMedia("print");return L(b(window,"beforeprint").pipe(l(()=>!0)),b(window,"afterprint").pipe(l(()=>!1))).pipe(N(e.matches))}function qr(e,t){return e.pipe(x(r=>r?t():R))}function ur(e,t={credentials:"same-origin"}){return ve(fetch(`${e}`,t)).pipe(ce(()=>R),x(r=>r.status!==200?Tt(()=>new Error(r.statusText)):H(r)))}function Ue(e,t){return ur(e,t).pipe(x(r=>r.json()),J(1))}function mo(e,t){let r=new DOMParser;return ur(e,t).pipe(x(n=>n.text()),l(n=>r.parseFromString(n,"text/xml")),J(1))}function pr(e){let t=M("script",{src:e});return I(()=>(document.head.appendChild(t),L(b(t,"load"),b(t,"error").pipe(x(()=>Tt(()=>new ReferenceError(`Invalid script: ${e}`))))).pipe(l(()=>{}),C(()=>document.head.removeChild(t)),Oe(1))))}function ho(){return{x:Math.max(0,scrollX),y:Math.max(0,scrollY)}}function bo(){return L(b(window,"scroll",{passive:!0}),b(window,"resize",{passive:!0})).pipe(l(ho),N(ho()))}function vo(){return{width:innerWidth,height:innerHeight}}function go(){return b(window,"resize",{passive:!0}).pipe(l(vo),N(vo()))}function yo(){return Q([bo(),go()]).pipe(l(([e,t])=>({offset:e,size:t})),J(1))}function lr(e,{viewport$:t,header$:r}){let n=t.pipe(X("size")),o=Q([n,r]).pipe(l(()=>Be(e)));return Q([r,t,o]).pipe(l(([{height:i},{offset:a,size:s},{x:f,y:c}])=>({offset:{x:a.x-f,y:a.y-c+i},size:s})))}(()=>{function e(n,o){parent.postMessage(n,o||"*")}function t(...n){return n.reduce((o,i)=>o.then(()=>new Promise(a=>{let s=document.createElement("script");s.src=i,s.onload=a,document.body.appendChild(s)})),Promise.resolve())}var r=class{constructor(n){this.url=n,this.onerror=null,this.onmessage=null,this.onmessageerror=null,this.m=a=>{a.source===this.w&&(a.stopImmediatePropagation(),this.dispatchEvent(new MessageEvent("message",{data:a.data})),this.onmessage&&this.onmessage(a))},this.e=(a,s,f,c,u)=>{if(s===this.url.toString()){let p=new ErrorEvent("error",{message:a,filename:s,lineno:f,colno:c,error:u});this.dispatchEvent(p),this.onerror&&this.onerror(p)}};let o=new EventTarget;this.addEventListener=o.addEventListener.bind(o),this.removeEventListener=o.removeEventListener.bind(o),this.dispatchEvent=o.dispatchEvent.bind(o);let i=document.createElement("iframe");i.width=i.height=i.frameBorder="0",document.body.appendChild(this.iframe=i),this.w.document.open(),this.w.document.write(` + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/azidjsclient

+

This library provides a thin wrapper around the @azure/identity library to make it easy to integrate Azure Identity authentication in your solution.

+

You will first need to install the package:

+

npm install @pnp/azidjsclient --save

+

The following example shows how to configure the SPFI or GraphFI object using this behavior.

+
import { DefaultAzureCredential } from "@azure/identity";
+import { spfi } from "@pnp/sp";
+import { graphfi } from "@pnp/sp";
+import { SPDefault, GraphDefault } from "@pnp/nodejs";
+import { AzureIdentity } from "@pnp/azidjsclient";
+import "@pnp/sp/webs";
+import "@pnp/graph/me";
+
+const credential = new DefaultAzureCredential();
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(
+    SPDefault(),
+    AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)
+);
+
+const graph = graphfi().using(
+    GraphDefault(),
+    AzureIdentity(credential, ["https://graph.microsoft.com/.default"], null)
+);
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+

Please see more scenarios in the authentication article.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/adv-clientside-pages/index.html b/concepts/adv-clientside-pages/index.html new file mode 100644 index 000000000..62223eec3 --- /dev/null +++ b/concepts/adv-clientside-pages/index.html @@ -0,0 +1,2819 @@ + + + + + + + + + + + + + + + + + + + + + + + + Client-Side Pages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Client-Side Pages

+

The client-side pages API included in this library is an implementation that was reverse engineered from the first-party API's and is unsupported by Microsoft. Given how flexible pages are we've done our best to give you the endpoints that will provide the functionality you need but that said, implementing these APIs is one of the more complicated tasks you can do.

+

It's especially important to understand the product team is constantly changing the features of pages and often that will also end up changing how the APIs that we've leveraged behave and because they are not offical third-party APIs this can cause our implementation to break. In order to fix those breaks we need to go back to the beginning and re-validate how the endpoints work searching for what has changed and then implementing those changes in our code. This is by no means simple. If you are reporting an issue with the pages API be aware that it may take significant time for us to unearth what is happening and fix it. Any research that you can provide when sharing your issue will go a long way in expediating that process, or better yet, if you can track it down and submit a PR with a fix we would be most greatful.

+

Tricks to help you figure out how to add first-party web parts to your page

+

This section is to offer you methods to be able to reverse engineer some of the first party web parts to help figure out how to add them to the page using the addControl method.

+

Your first step needs to be creating a test page that you can inspect.

+
    +
  1. Create a new Site Page.
  2. +
  3. Open the browser console, and navigate to the network tab
  4. +
  5. Filter the network tab for Fetch/XHR and then type SavePage to filter for the specific network calls.
  6. +
  7. Add and configure the web part you want to reverse engineer and then save the page as draft. The network tab will now show a SavePageAsDraft call and you can then look at the Payload of that call + Client-Side Pages Network Tab Image
  8. +
  9. You then want to specifically look at the CanvasContent1 property and copy that value. You can then paste it into a temporary file with the .json extension in your code editor so you can inspect the payload. The value is an array of objects, and each object (except the last) is the definition of the web part.
  10. +
+

Below is an example (as of the last change date of this document) of what the QuickLinks web part looks like. One key takeaway from this file is the webPartId property which can be used when filtering for the right web part definition after getting a collection from sp.web.getClientsideWebParts();.

+
+

Note that it could change at any time so please do not rely on this data, please use it as an example only.

+
+
{
+    "position": {
+        "layoutIndex": 1,
+        "zoneIndex": 1,
+        "sectionIndex": 1,
+        "sectionFactor": 12,
+        "controlIndex": 1
+    },
+    "controlType": 3,
+    "id": "00000000-58fd-448c-9e40-6691ce30e3e4",
+    "webPartId": "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd",
+    "addedFromPersistedData": true,
+    "reservedHeight": 141,
+    "reservedWidth": 909,
+    "webPartData": {
+        "id": "c70391ea-0b10-4ee9-b2b4-006d3fcad0cd",
+        "instanceId": "00000000-58fd-448c-9e40-6691ce30e3e4",
+        "title": "Quick links",
+        "description": "Show a collection of links to content such as documents, images, videos, and more in a variety of layouts with options for icons, images, and audience targeting.",
+        "audiences": [],
+        "serverProcessedContent": {
+            "htmlStrings": {},
+            "searchablePlainTexts": {
+                "items[0].title": "PnPjs Title"
+            },
+            "imageSources": {},
+            "links": {
+                "baseUrl": "https://contoso.sharepoint.com/sites/PnPJS",
+                "items[0].sourceItem.url": "/sites/PnPJS/SitePages/pnpjsTestV2.aspx"
+            },
+            "componentDependencies": {
+                "layoutComponentId": "706e33c8-af37-4e7b-9d22-6e5694d92a6f"
+            }
+        },
+        "dataVersion": "2.2",
+        "properties": {
+            "items": [
+                {
+                    "sourceItem": {
+                        "guids": {
+                            "siteId": "00000000-4657-40d2-843d-3d6c72e647ff",
+                            "webId": "00000000-e714-4de6-88db-b0ac40d17850",
+                            "listId": "{00000000-8ED8-4E43-82BD-56794D9AB290}",
+                            "uniqueId": "00000000-6779-4979-adad-c120a39fe311"
+                        },
+                        "itemType": 0,
+                        "fileExtension": ".ASPX",
+                        "progId": null
+                    },
+                    "thumbnailType": 2,
+                    "id": 1,
+                    "description": "",
+                    "fabricReactIcon": {
+                        "iconName": "heartfill"
+                    },
+                    "altText": "",
+                    "rawPreviewImageMinCanvasWidth": 32767
+                }
+            ],
+            "isMigrated": true,
+            "layoutId": "CompactCard",
+            "shouldShowThumbnail": true,
+            "imageWidth": 100,
+            "buttonLayoutOptions": {
+                "showDescription": false,
+                "buttonTreatment": 2,
+                "iconPositionType": 2,
+                "textAlignmentVertical": 2,
+                "textAlignmentHorizontal": 2,
+                "linesOfText": 2
+            },
+            "listLayoutOptions": {
+                "showDescription": false,
+                "showIcon": true
+            },
+            "waffleLayoutOptions": {
+                "iconSize": 1,
+                "onlyShowThumbnail": false
+            },
+            "hideWebPartWhenEmpty": true,
+            "dataProviderId": "QuickLinks",
+            "webId": "00000000-e714-4de6-88db-b0ac40d17850",
+            "siteId": "00000000-4657-40d2-843d-3d6c72e647ff"
+        }
+    }
+}
+
+

At this point the only aspect of the above JSON payload you're going to be paying attention to is the webPartData. We have exposed title, description, and dataVersion as default properties of the ClientsideWebpart class. In addition we provide a getProperties, setProperties, getServerProcessedContent, setServerProcessedContent methods. The difference in this case in these set base methods is that it will merge the object you pass into those methods with the values already on the object.

+

The code below gives a incomplete but demonstrative example of how you would extend the ClientsideWebpart class to provide an interface to build a custom class for the QuickLinks web part illustrated in our JSON payload above. This code assumes you have already added the control to a section. For more information about that step see the documentation for Add Controls

+
import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
+
+//Define interface based on JSON object above
+interface IQLItem {
+    sourceItem: {
+        guids: {
+            siteId: string;
+            webId:  string;
+            listId:  string;
+            uniqueId:  string;
+        },
+        itemType: number;
+        fileExtension: string;
+        progId: string;
+    }
+    thumbnailType: number;
+    id: number;
+    description: string;
+    fabricReactIcon: { iconName: string; };
+    altText: string;
+    rawPreviewImageMinCanvasWidth: number;
+}
+
+// we create a class to wrap our functionality in a reusable way
+class QuickLinksWebpart extends ClientsideWebpart {
+
+  constructor(control: ClientsideWebpart) {
+    super((<any>control).json);
+  }
+
+  // add property getter/setter for what we need, in this case items array within properties
+  public get items(): IQLItem[] {
+    return this.json.webPartData?.properties?.items || [];
+  }
+
+  public set items(value: IQLItem[]) {
+    this.json.webPartData.properties?.items = value;
+  }
+}
+
+// now we load our page
+const page = await sp.web.loadClientsidePage("/sites/PnPJS/SitePages/QuickLinks-Web-Part-Test.aspx");
+
+// get our part and pass it to the constructor of our wrapper class.
+const part = new QuickLinksWebpart(page.sections[0].columns[0].getControl(0));
+
+//Need to set all the properties
+part.items = [{IQLItem_properties}];
+
+await page.save();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/auth-browser/index.html b/concepts/auth-browser/index.html new file mode 100644 index 000000000..41bd3e2f9 --- /dev/null +++ b/concepts/auth-browser/index.html @@ -0,0 +1,2692 @@ + + + + + + + + + + + + + + + + + + + + + + + + In Browser - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Authentication in a custom browser based application

+

We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.

+

npm install @pnp/msaljsclient --save

+

At this time we're using version 1.x of the msal library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.

+

Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:

+
import { Configuration, AuthenticationParameters } from "msal";
+
+const configuration: Configuration = {
+  auth: {
+    authority: "https://login.microsoftonline.com/{tenant Id}/",
+    clientId: "{AAD Application Id/Client Id}"
+  }
+};
+
+const authParams: AuthenticationParameters = {
+  scopes: ["https://graph.microsoft.com/.default"] 
+};
+
+

MSAL + Browser

+
import { spfi, SPBrowser } from "@pnp/sp";
+import { graphfi, GraphBrowser } from "@pnp/graph";
+import { MSAL } from "@pnp/msaljsclient";
+import "@pnp/sp/webs";
+import "@pnp/graph/users";
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(configuration, authParams));
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const graph = graphfi().using(GraphBrowser(), MSAL(configuration, authParams));
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/auth-nodejs/index.html b/concepts/auth-nodejs/index.html new file mode 100644 index 000000000..382296256 --- /dev/null +++ b/concepts/auth-nodejs/index.html @@ -0,0 +1,2769 @@ + + + + + + + + + + + + + + + + + + + + + + + + In NodeJS - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Authentication in NodeJS

+

We support MSAL for both browser and nodejs and Azure Identity for nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible configurations, but any parameters supplied are passed through to the underlying implementation.

+

Depending on which package you want to use you will need to install an additional package from the library because of the large dependencies.

+

We support MSAL through the msal-node library which is included by the @pnp/nodejs package.

+

For the Azure Identity package:

+

npm install @pnp/azidjsclient --save

+

We support Azure Identity through the @azure/identity library which simplifies the authentication process and makes it easy to integrate Azure Identity authentication in your solution.

+

MSAL + NodeJS

+

The SPDefault and GraphDefault exported by the nodejs library include MSAL and takes the parameters directly.

+

The following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:

+
import { SPDefault, GraphDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+import { graphfi } from "@pnp/graph";
+import { Configuration, AuthenticationParameters } from "msal";
+import "@pnp/graph/users";
+import "@pnp/sp/webs";
+
+const configuration: Configuration = {
+  auth: {
+    authority: "https://login.microsoftonline.com/{tenant Id}/",
+    clientId: "{AAD Application Id/Client Id}"
+  }
+};
+
+const sp = spfi("{site url}").using(SPDefault({
+    msal: {
+        config: configuration,
+        scopes: ["https://{tenant}.sharepoint.com/.default"],
+    },
+}));
+
+const graph = graphfi().using(GraphDefault({
+    msal: {
+        config: configuration,
+        scopes: ["https://graph.microsoft.com/.default"],
+    },
+}));
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+

Use Nodejs MSAL behavior directly

+

It is also possible to use the MSAL behavior directly if you are composing your own strategies.

+
import { SPDefault, GraphDefault, MSAL } from "@pnp/nodejs";
+
+const sp = spfi("{site url}").using(SPDefault(), MSAL({
+  config: configuration,
+  scopes: ["https://{tenant}.sharepoint.com/.default"],
+}));
+
+const graph = graphfi().using(GraphDefault(), MSAL({
+  config: configuration,
+  scopes: ["https://graph.microsoft.com/.default"],
+}));
+
+
+

Azure Identity + NodeJS

+

The following sample shows how to pass the credential object to the AzureIdentity behavior including scopes.

+
import { DefaultAzureCredential } from "@azure/identity";
+import { spfi } from "@pnp/sp";
+import { graphfi } from "@pnp/sp";
+import { SPDefault, GraphDefault } from "@pnp/nodejs";
+import { AzureIdentity } from "@pnp/azidjsclient";
+import "@pnp/sp/webs";
+import "@pnp/graph/users";
+
+// We're using DefaultAzureCredential but the credential can be any valid `Credential Type`
+const credential = new DefaultAzureCredential();
+
+const sp = spfi("https://{tenant}.sharepoint.com/sites/dev").using(
+    SPDefault(),
+    AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)
+);
+
+const graph = graphfi().using(
+    GraphDefault(),
+    AzureIdentity(credential, ["https://graph.microsoft.com/.default"], null)
+);
+
+const webData = await sp.web();
+const meData = await graph.me();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/auth-spfx/index.html b/concepts/auth-spfx/index.html new file mode 100644 index 000000000..dd4a8c252 --- /dev/null +++ b/concepts/auth-spfx/index.html @@ -0,0 +1,2758 @@ + + + + + + + + + + + + + + + + + + + + + + + + In SPFx - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Authentication in SharePoint Framework

+

When building in SharePoint Framework you only need to provide the context to either sp or graph to ensure proper authentication. This will use the default SharePoint AAD application to manage scopes. If you would prefer to use a different AAD application please see the MSAL section below.

+

SPFx + SharePoint

+
import { SPFx, spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const sp = spfi().using(SPFx(this.context));
+
+const webData = await sp.web();
+
+

SPFx + Graph

+
import { SPFx, graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const graph = graphfi().using(SPFx(this.context));
+
+const meData = await graph.me();
+
+

SPFx + Authentication Token

+

When using the SPFx behavior, authentication is handled by a cookie stored on the users client. In very specific instances some of the SharePoint methods will require a token. We have added a custom behavior to support that called SPFxToken. This will require that you add the appropriate application role to the SharePoint Framework's package-solution.json -> webApiPermissionRequests section where you will define the resource and scope for the request.

+

Here's an example of how you would build an instance of the SPFI that would include an Bearer Token in the header. Be advised if you use this instance to make calls to SharePoint endpoints that you have not specifically authorized they will fail.

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+

MSAL + SPFx

+

We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.

+

npm install @pnp/msaljsclient --save

+

At this time we're using version 1.x of the msal library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.

+

Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:

+
import { SPFx as graphSPFx, graphfi } from "@pnp/graph";
+import { SPFx as spSPFx, spfi } from "@pnp/sp";
+import { MSAL } from "@pnp/msaljsclient";
+import { Configuration, AuthenticationParameters } from "msal";
+import "@pnp/graph/users";
+import "@pnp/sp/webs";
+
+const configuration: Configuration = {
+  auth: {
+    authority: "https://login.microsoftonline.com/{tenant Id}/",
+    clientId: "{AAD Application Id/Client Id}"
+  }
+};
+
+const authParams: AuthenticationParameters = {
+  scopes: ["https://graph.microsoft.com/.default"] 
+};
+
+// within a webpart, application customizer, or adaptive card extension where the context object is available
+const graph = graphfi().using(graphSPFx(this.context), MSAL(configuration, authParams));
+const sp = spfi().using(spSPFx(this.context), MSAL(configuration, authParams));
+
+const meData = await graph.me();
+const webData = await sp.web();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/authentication/index.html b/concepts/authentication/index.html new file mode 100644 index 000000000..bd204041a --- /dev/null +++ b/concepts/authentication/index.html @@ -0,0 +1,2648 @@ + + + + + + + + + + + + + + + + + + + + + + + + Authentication - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Authentication

+

One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods.

+

We provide multiple ways to authenticate based on the scenario you're developing for, see one of these more detailed guides:

+ +

If you have more specific authentication requirements you can always build your own by using the new queryable pattern which exposes a dedicated auth moment. That moment expects observers with the signature:

+
async function(url, init) {
+
+  // logic to apply authentication to the request
+
+    return [url, init];
+}
+
+

You can follow this example as a general pattern to build your own custom authentication model. You can then wrap your authentication in a behavior for easy reuse.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using({behaviors});
+const web = sp.web;
+
+// we will use custom auth on this web
+web.on.auth(async function(url, init) {
+
+    // some code to get a token
+    const token = getToken();
+
+    // set the Authorization header in the init (this init is what is passed directly to the fetch call)
+    init.headers["Authorization"] = `Bearer ${token}`;
+
+    return [url, init];
+});
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/batching-caching/index.html b/concepts/batching-caching/index.html new file mode 100644 index 000000000..2fe317f55 --- /dev/null +++ b/concepts/batching-caching/index.html @@ -0,0 +1,2684 @@ + + + + + + + + + + + + + + + + + + + + + + + + Batching & Caching - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Batching and Caching

+

When optimizing for performance you can combine batching and caching to reduce the overall number of requests. On the first request any cachable data is stored as expected once the request completes. On subsequent requests if data is found in the cache it is returned immediately and that request is not added to the batch, in fact the batch will never register the request. This can work across many requests such that some returned cached data and others do not - the non-cached requests will be added to and processed by the batch as expected.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { Caching } from "@pnp/queryable";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+batchedSP.using(Caching());
+
+batchedSP.web().then(console.log);
+
+batchedSP.web.lists().then(console.log);
+
+// execute the first set of batched requests, no information is currently cached
+await execute();
+
+// create a new batch
+const [batchedSP2, execute2] = await sp.batched();
+batchedSP2.using(Caching());
+
+// add the same requests - this simulates the user navigating away from or reloading the page
+batchedSP2.web().then(console.log);
+batchedSP2.web.lists().then(console.log);
+
+// executing the batch will return the cached values from the initial requests
+await execute2();
+
+

In this second example we include an update to the web's title. Because non-get requests are never cached the update code will always run, but the results from the two get requests will resolve from the cache prior to being added to the batch.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { Caching } from "@pnp/queryable";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+batchedSP.using(Caching());
+
+batchedSP.web().then(console.log);
+
+batchedSP.web.lists().then(console.log);
+
+// this will never be cached
+batchedSP.web.update({
+    Title: "dev web 1",
+});
+
+// execute the first set of batched requests, no information is currently cached
+await execute();
+
+// create a new batch
+const [batchedSP2, execute2] = await sp.batched();
+batchedSP2.using(Caching());
+
+// add the same requests - this simulates the user navigating away from or reloading the page
+batchedSP2.web().then(console.log);
+batchedSP2.web.lists().then(console.log);
+
+// this will never be cached
+batchedSP2.web.update({
+    Title: "dev web 2",
+});
+
+// executing the batch will return the cached values from the initial requests
+await execute2();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/batching/index.html b/concepts/batching/index.html new file mode 100644 index 000000000..3dde9a366 --- /dev/null +++ b/concepts/batching/index.html @@ -0,0 +1,2972 @@ + + + + + + + + + + + + + + + + + + + + + + + + Batching - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

Batching

+

Where possible batching can significantly increase application performance by combining multiple requests to the server into one. This is especially useful when first establishing state, but applies for any scenario where you need to make multiple requests before loading or based on a user action. Batching is supported within the sp and graph libraries as shown below.

+

SP Example

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/batching";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+let res = [];
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+batchedSP.web().then(r => res.push(r));
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+// ODATA operations such as select, filter, and expand are supported as normal
+batchedSP.web.lists.select("Title")().then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i = 0; i < res.length; i++) {
+    ///Do something with the results
+}
+
+

Using a batched web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/batching";
+
+const sp = spfi(...);
+
+const [batchedWeb, execute] = sp.web.batched();
+
+let res = [];
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+batchedWeb().then(r => res.push(r));
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+// ODATA operations such as select, filter, and expand are supported as normal
+batchedWeb.lists.select("Title")().then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i = 0; i < res.length; i++) {
+    ///Do something with the results
+}
+
+
+

Batches must be for the same web, you cannot combine requests from multiple webs into a batch.

+
+

Graph Example

+
import { graphfi } from "@pnp/graph";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+import "@pnp/graph/batching";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+const [batchedGraph, execute] = graph.batched();
+
+let res = [];
+
+// Pushes the results of these calls to an array
+// you need to use .then syntax here as otherwise the application will stop and await the result
+batchedGraph.users().then(r => res.push(r));
+
+// you need to use .then syntax here as otherwise the application will stop and await the result
+// ODATA operations such as select, filter, and expand are supported as normal
+batchedGraph.groups.select("Id")().then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i=0; i<res.length; i++){
+    // Do something with the results
+}
+
+

Advanced Batching

+

For most cases the above usage should be sufficient, however you may be in a situation where you do not have convenient access to either an spfi instance or a web. Let's say for example you want to add a lot of items to a list and have an IList. You can in these cases use the createBatch function directly. We recommend as much as possible using the sp or web or graph batched method, but also provide this additional flexibility if you need it.

+
import { createBatch } from "@pnp/sp/batching";
+import { SPDefault } from "@pnp/nodejs";
+import { IList } from "@pnp/sp/lists";
+import "@pnp/sp/items/list";
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPDefault({ /* ... */ }));
+
+// in one part of your application you setup a list instance
+const list: IList = sp.web.lists.getByTitle("MyList");
+
+
+// in another part of your application you want to batch requests, but do not have the sp instance available, just the IList
+
+// note here the first part of the tuple is NOT the object, rather the behavior that enables batching. You must still register it with `using`.
+const [batchedListBehavior, execute] = createBatch(list);
+// this list is now batching all its requests
+list.using(batchedListBehavior);
+
+// these will all occur within a single batch
+list.items.add({ Title: `1: ${getRandomString(4)}` });
+list.items.add({ Title: `2: ${getRandomString(4)}` });
+list.items.add({ Title: `3: ${getRandomString(4)}` });
+list.items.add({ Title: `4: ${getRandomString(4)}` });
+
+await execute();
+
+

This is of course also possible with the graph library as shown below.

+
import { graphfi } from "@pnp/graph";
+import { createBatch } from "@pnp/graph/batching";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+const users = graph.users;
+
+const [batchedBehavior, execute] = createBatch(users);
+users.using(batchedBehavior);
+
+users();
+// we can only place the 'users' instance into the batch once
+graph.users.using(batchedBehavior)();
+graph.users.using(batchedBehavior)();
+graph.users.using(batchedBehavior)();
+
+await execute();       
+
+

+

Don't reuse objects in Batching

+

It shouldn't come up often, but you can not make multiple requests using the same instance of a queryable in a batch. Let's consider the incorrect example below:

+
+

The error message will be "This instance is already part of a batch. Please review the docs at https://pnp.github.io/pnpjs/concepts/batching#reuse."

+
+
import { graphfi } from "@pnp/graph";
+import { createBatch } from "@pnp/graph/batching";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+// gain a batched instance of the graph
+const [batchedGraph, execute] = graph.batched();
+
+// we take a reference to the value returned from .users
+const users = batchedGraph.users;
+
+// we invoke it, adding it to the batch (this is a request to /users), it will succeed
+users();
+
+// we invoke it again, because this instance has already been added to the batch, this request will throw an error
+users();
+
+// we execute the batch, this promise will resolve
+await execute();        
+
+

To overcome this you can either start a new fluent chain or use the factory method. Starting a new fluent chain at any point will create a new instance. Please review the corrected sample below.

+
import { graphfi } from "@pnp/graph";
+import { createBatch } from "@pnp/graph/batching";
+import { GraphDefault } from "@pnp/nodejs";
+import { Users } from "@pnp/graph/users";
+
+const graph = graphfi().using(GraphDefault({ /* ... */ }));
+
+// gain a batched instance of the graph
+const [batchedGraph, execute] = graph.batched();
+
+// we invoke a new instance of users from the batchedGraph
+batchedGraph.users();
+
+// we again invoke a new instance of users from the batchedGraph, this is fine
+batchedGraph.users();
+
+const users = batchedGraph.users;
+// we can do this once
+users();
+
+// by creating a new users instance using the Users factory we can keep adding things to the batch
+// users2 will be part of the same batch
+const users2 = Users(users);
+users2();
+
+// we execute the batch, this promise will resolve
+await execute();        
+
+
+

In addition you cannot continue using a batch after execute. Once execute has resolved the batch is done. You should create a new batch using one of the described methods to conduct another batched call.

+
+

Case where batch result returns an object that can be invoked

+

In the following example, the results of adding items to the list is an object with a type of IItemAddResult which is {data: any, item: IItem}. Since version v1 the expectation is that the item object is immediately usable to make additional queries. When this object is the result of a batched call, this was not the case so we have added additional code to reset the observers using the original base from witch the batch was created, mimicing the behavior had the IItem been created from that base withyout a batch involved. We use CopyFrom to ensure that we maintain the references to the InternalResolve and InternalReject events through the end of this timelines lifecycle.

+
import { createBatch } from "@pnp/sp/batching";
+import { SPDefault } from "@pnp/nodejs";
+import { IList } from "@pnp/sp/lists";
+import "@pnp/sp/items/list";
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPDefault({ /* ... */ }));
+
+// in one part of your application you setup a list instance
+const list: IList = sp.web.lists.getByTitle("MyList");
+
+const [batchedListBehavior, execute] = createBatch(list);
+// this list is now batching all its requests
+list.using(batchedListBehavior);
+
+let res: IItemAddResult[] = [];
+
+// these will all occur within a single batch
+list.items.add({ Title: `1: ${getRandomString(4)}` }).then(r => res.push(r));
+list.items.add({ Title: `2: ${getRandomString(4)}` }).then(r => res.push(r));
+list.items.add({ Title: `3: ${getRandomString(4)}` }).then(r => res.push(r));
+list.items.add({ Title: `4: ${getRandomString(4)}` }).then(r => res.push(r));
+
+await execute();
+
+let newItems: IItem[] = [];
+
+for(let i=0; i<res.length; i++){
+    //This line will correctly resolve
+    const newItem = await res[i].item.select("Title")<{Title: string}>();
+    newItems.push(newItem);
+}
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/calling-other-endpoints/index.html b/concepts/calling-other-endpoints/index.html new file mode 100644 index 000000000..bba3f2a33 --- /dev/null +++ b/concepts/calling-other-endpoints/index.html @@ -0,0 +1,2845 @@ + + + + + + + + + + + + + + + + + + + + Calling other endpoints not currently implemented in PnPjs library - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Calling other endpoints not currently implemented in PnPjs library

+

If you find that there are endpoints that have not yet been implemented, or have changed in such a way that there are issues using the implemented endpoint, you can still make those calls and take advantage of the plumbing provided by the library.

+

SharePoint

+

To issue calls against the SharePoint REST endpoints you would use one of the existing operations:

+
    +
  • spGet
  • +
  • spPost
  • +
  • spDelete
  • +
  • spPatch +and the extended post methods with additional headers.
  • +
  • spPostMerge
  • +
  • spPostDelete
  • +
  • spPostDeleteETag
  • +
+

To construct a call you will need to pass, to the operation call an SPQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.

+

Below are a couple of examples to get you started.

+

Example spGet

+

Let's pretend that the getById method didn't exist on a lists items. The example below shows two methods for constructing our SPQueryable method.

+

The first is the easiest to use because, as the queryable documentation tells us, this will maintain all the registered observers on the original queryable instance. We would start with the queryable object closest to the endpoint we want to use, in this case list. We do this because we need to construct the full URL that will be called. Using list in this instance gives us the first part of the URL (e.g. https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')) and then we can construct the remainder of the call by passing in a string.

+

The second method essentially starts from scratch where the user constructs the entire url and then registers observers on the SPQuerable instance. Then uses spGet to execute the call. There are many other variations to arrive at the same outcome, all are dependent on your requirements.

+
import { spfi } from "@pnp/sp";
+import { AssignFrom } from "@pnp/core";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { spGet, SPQueryable, SPFx } from "@pnp/sp";
+
+// Establish SPFI instance passing in the appropriate behavior to register the initial observers.
+const sp = spfi(...);
+
+// create an instance of the items queryable
+
+const list = sp.web.lists.getByTitle("My List");
+
+// get the item with an id of 1, easiest method
+const item: any = await spGet(SPQueryable(list, "items(1)"));
+
+// get the item with an id of 1, constructing a new queryable and registering behaviors
+const spQueryable = SPQueryable("https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)").using(SPFx(this.context));
+
+// ***or***
+
+// For v3 the full url is require for SPQuerable when providing just a string
+const spQueryable = SPQueryable("https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)").using(AssignFrom(sp.web));
+
+// and then use spQueryable to make the request
+const item: any = await spGet(spQueryable);
+
+

The resulting call will be to the endpoint: +https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)

+

Example spPost

+

Let's now pretend that we need to get the changes on a list and want to call the getchanges method off list.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { IChangeQuery, spPost, SPQueryable } from "@pnp/sp";
+import { body } from "@pnp/queryable";
+
+// Establish SPFI instance passing in the appropriate behavior to register the initial observers.
+const sp = spfi(...);
+
+
+// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore
+const query: IChangeQuery = {
+    Add: true,
+    ChangeTokenEnd: null,
+    ChangeTokenStart: null,
+    DeleteObject: true,
+    Rename: true,
+    Restore: true,
+};
+
+// create an instance of the items queryable
+const list = sp.web.lists.getByTitle("My List");
+
+// get the item with an id of 1
+const changes: any = await spPost(SPQueryable(list, "getchanges"), body({query}));
+
+
+

The resulting call will be to the endpoint: +https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/getchanges

+

Microsoft Graph

+

To issue calls against the Microsoft Graph REST endpoints you would use one of the existing operations:

+
    +
  • graphGet
  • +
  • graphPost
  • +
  • graphDelete
  • +
  • graphPatch
  • +
  • graphPut
  • +
+

To construct a call you will need to pass, to the operation call an GraphQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.

+

Below are a couple of examples to get you started.

+

Example graphGet

+

Here's an example for getting the chats for a particular user. This uses the simplest method for constructing the graphQueryable which is to start with a instance of a queryable that is close to the endpoint we want to call, in this case user and then adding the additional path as a string. For a more advanced example see spGet above.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import { GraphQueryable, graphGet } from "@pnp/graph";
+
+// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.
+const graph = graphfi(...);
+
+// create an instance of the user queryable
+const user = graph.users.getById('jane@contoso.com');
+
+// get the chats for the user
+const chat: any = await graphGet(GraphQueryable(user, "chats"));
+
+

The results call will be to the endpoint: +https://graph.microsoft.com/v1.0/users/jane@contoso.com/chats

+

Example graphPost

+

This is an example of adding an event to a calendar.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/calendars";
+import { GraphQueryable, graphPost } from "@pnp/graph";
+import { body, InjectHeaders } from "@pnp/queryable";
+
+// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.
+const graph = graphfi(...);
+
+// create an instance of the user queryable
+const calendar = graph.users.getById('jane@contoso.com').calendar;
+
+const props = {
+  "subject": "Let's go for lunch",
+  "body": {
+    "contentType": "HTML",
+    "content": "Does noon work for you?"
+  },
+  "start": {
+      "dateTime": "2017-04-15T12:00:00",
+      "timeZone": "Pacific Standard Time"
+  },
+  "end": {
+      "dateTime": "2017-04-15T14:00:00",
+      "timeZone": "Pacific Standard Time"
+  },
+  "location":{
+      "displayName":"Harry's Bar"
+  },
+  "attendees": [
+    {
+      "emailAddress": {
+        "address":"samanthab@contoso.onmicrosoft.com",
+        "name": "Samantha Booth"
+      },
+      "type": "required"
+    }
+  ],
+  "allowNewTimeProposals": true,
+  "transactionId":"7E163156-7762-4BEB-A1C6-729EA81755A7"
+};
+
+// custom request init to add timezone header.
+const graphQueryable = GraphQueryable(calendar, "events").using(InjectHeaders({
+    "Prefer": 'outlook.timezone="Pacific Standard Time"',
+}));
+
+// adds a new event to the user's calendar
+const event: any = await graphPost(graphQueryable, body(props));
+
+

The results call will be to the endpoint: +https://graph.microsoft.com/v1.0/users/jane@contoso.com/calendar/events

+

Advanced Scenario

+

If you find you need to create an instance of Queryable (for either graph or SharePoint) that would hang off the root of the url you can use the AssignFrom or CopyFrom behaviors.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import { GraphQueryable, graphPost } from "@pnp/graph";
+import { body, InjectHeaders } from "@pnp/queryable";
+import { AssignFrom } from "@pnp/core";
+
+// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.
+const graph = graphfi(...);
+
+const chatsQueryable = GraphQueryable("chats").using(AssignFrom(graph.me));
+
+const chat: any = await graphPost(chatsQueryable, body(chatBody));
+
+

The results call will be to the endpoint: +https://graph.microsoft.com/v1.0/chats

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/custom-bundle/index.html b/concepts/custom-bundle/index.html new file mode 100644 index 000000000..434444aab --- /dev/null +++ b/concepts/custom-bundle/index.html @@ -0,0 +1,2696 @@ + + + + + + + + + + + + + + + + + + + + + + + + Custom Bundle - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Custom Bundling

+

With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles.

+

Scenarios could include:

+
    +
  • Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once.
  • +
  • Creating SPFx libraries either for one project or a single webpart.
  • +
  • Create a single library containing the PnPjs code you need bundled along with your custom extensions.
  • +
+

Create a custom bundle

+

Webpack

+

You can see/clone a sample project of this example here.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/error-handling/index.html b/concepts/error-handling/index.html new file mode 100644 index 000000000..b8644bb08 --- /dev/null +++ b/concepts/error-handling/index.html @@ -0,0 +1,3020 @@ + + + + + + + + + + + + + + + + + + + + + + + + Error-Handling - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Error Handling

+

This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.

+
+

For 429, 503, and 504 errors we include retry logic within the library

+
+

The HttpRequestError

+

All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error. In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Property NameDescription
nameStandard Error.name property. Always 'Error'
messageNormalized string containing the status, status text, and the full response text
stackThe callstack producing the error
isHttpRequestErrorAlways true, allows you to reliably determine if you have an HttpRequestError instance
responseUnread copy of the Response object resulting in the thrown error
statusThe Response.status value (such as 404)
statusTextThe Response.statusText value (such as 'Not Found')
+

Basic Handling

+

For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen 😉. The most basic type of error handling involves a simple try-catch when using the async/await promises pattern.

+
import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+try {
+
+  // get a list that doesn't exist
+  const w = await sp.web.lists.getByTitle("no")();
+
+} catch (e) {
+
+  console.error(e);
+}
+
+

This will produce output like:

+
Error making HttpClient request in queryable [404] Not Found ::> {"odata.error":{"code":"-1, System.ArgumentException","message":{"lang":"en-US","value":"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'."}}} Data: {"response":{"size":0,"timeout":0},"status":404,"statusText":"Not Found","isHttpRequestError":true}
+
+

This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.

+

Reading the Response

+

In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire:

+
import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { HttpRequestError } from "@pnp/queryable";
+
+try {
+
+  // get a list that doesn't exist
+  const w = await sp.web.lists.getByTitle("no")();
+
+} catch (e) {
+
+  // are we dealing with an HttpRequestError?
+  if (e?.isHttpRequestError) {
+
+    // we can read the json from the response
+    const json = await (<HttpRequestError>e).response.json();
+
+    // if we have a value property we can show it
+    console.log(typeof json["odata.error"] === "object" ? json["odata.error"].message.value : e.message);
+
+    // add of course you have access to the other properties and can make choices on how to act
+    if ((<HttpRequestError>e).status === 404) {
+       console.error((<HttpRequestError>e).statusText);
+      // maybe create the resource, or redirect, or fallback to a secondary data source
+      // just ideas, handle any of the status codes uniquely as needed
+    }
+
+  } else {
+    // not an HttpRequestError so we just log message
+    console.log(e.message);
+  }
+}
+
+

Logging errors

+

Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework.

+
import { Logger } from "@pnp/logging";
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+try {
+  // get a list that doesn't exist
+  const w = await sp.web.lists.getByTitle("no")();  
+} catch (e) {
+
+  Logger.error(e);
+}
+
+

You may want to read the response and customize the message as described above:

+
import { Logger } from "@pnp/logging";
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { HttpRequestError } from "@pnp/queryable";
+
+try {
+  // get a list that doesn't exist
+  const w = await sp.web.lists.getByTitle("no")();  
+} catch (e) {
+
+  if (e?.isHttpRequestError) {
+
+    // we can read the json from the response
+    const data = await (<HttpRequestError>e).response.json();
+
+    // parse this however you want
+    const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;
+
+    // we use the status to determine a custom logging level
+    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;
+
+    // create a custom log entry
+    Logger.log({
+      data,
+      level,
+      message,
+    });
+
+  } else {
+    // not an HttpRequestError so we just log message
+    Logger.error(e);
+  }
+}
+
+

Putting it All Together

+

After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.

+

errorhandler.ts

+
import { Logger } from "@pnp/logging";
+import { HttpRequestError } from "@pnp/queryable";
+import { hOP } from "@pnp/core";
+
+export async function handleError(e: Error | HttpRequestError): Promise<void> {
+
+  if (hOP(e, "isHttpRequestError")) {
+
+    // we can read the json from the response
+    const data = await (<HttpRequestError>e).response.json();
+
+    // parse this however you want
+    const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;
+
+    // we use the status to determine a custom logging level
+    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;
+
+    // create a custom log entry
+    Logger.log({
+      data,
+      level,
+      message,
+    });
+
+  } else {
+    // not an HttpRequestError so we just log message
+    Logger.error(e);
+  }
+}
+
+

web-request.ts

+
import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { handleError } from "./errorhandler";
+
+try {
+
+  const w = await sp.web.lists.getByTitle("no")();
+
+} catch (e) {
+
+  await handleError(e);
+}
+
+

web-request2.ts

+
import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { handleError } from "./errorhandler";
+
+try {
+
+  const w = await sp.web.lists();
+
+} catch (e) {
+
+  await handleError(e);
+}
+
+

Building a Custom Error Handler

+

In Version 3 the library introduced the concept of a Timeline object and moments. One of the broadcast moments is error. To create your own custom error handler you can define a special handler for the error moment something like the following:

+

+//Custom Error Behavior
+export function CustomError(): TimelinePipe<Queryable> {
+
+    return (instance: Queryable) => {
+
+        instance.on.error((err) => {
+            if (logging) {
+                console.log(`🛑 PnPjs Testing Error - ${err.toString()}`);
+            }
+        });
+
+        return instance;
+    };
+}
+
+//Adding our CustomError behavior to our timline
+
+const sp = spfi().using(SPDefault(this.context)).using(CustomError());
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/invokable/index.html b/concepts/invokable/index.html new file mode 100644 index 000000000..6b1b8d8ce --- /dev/null +++ b/concepts/invokable/index.html @@ -0,0 +1,2617 @@ + + + + + + + + + + + + + + + + + + + + + + + + Custom Call/Path - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Invokables

+

For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain: Starting with v3 this is no longer possible, you must invoke the object directly to execute the default action for that class:

+
const lists = await sp.web.lists();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/nightly-builds/index.html b/concepts/nightly-builds/index.html new file mode 100644 index 000000000..93c1a8e23 --- /dev/null +++ b/concepts/nightly-builds/index.html @@ -0,0 +1,2685 @@ + + + + + + + + + + + + + + + + + + + + + + + + Nightly Builds - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Nightly Builds

+

Starting with version 3 we support nightly builds, which are built from the version-3 branch each evening and include all the changes merged ahead of a particular build. These are a great way to try out new features before a release, or get a fix or enhancement without waiting for the monthly builds.

+

You can install the nightly builds using the below examples. While we only show examples for sp and graph nightly builds are available for all packages.

+

SP

+
npm install @pnp/sp@v3nightly --save
+
+

Microsoft Graph

+
npm install @pnp/graph@v3nightly --save
+
+
+

Nightly builds are NOT monthly releases and aren't tested as deeply. We never intend to release broken code, but nightly builds may contain some code that is not entirely final or fully reviewed. As always if you encounter an issue please let us know, especially for nightly builds so we can be sure to address it before the next monthly release.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/project-preset/index.html b/concepts/project-preset/index.html new file mode 100644 index 000000000..ddbb208d4 --- /dev/null +++ b/concepts/project-preset/index.html @@ -0,0 +1,2789 @@ + + + + + + + + + + + + + + + + + + + + + + + + Project Config/Services Setup - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Project Config/Services Setup

+

Due to the introduction of selective imports it can be somewhat frustrating to import all of the needed dependencies every time you need them across many files. Instead the preferred approach, especially for SPFx, is to create a project config file or establish a service to manage your PnPjs interfaces. Doing so centralizes the imports, configuration, and optionally extensions to PnPjs in a single place.

+
+

If you have multiple projects that share dependencies on PnPjs you can benefit from creating a custom bundle and using them across your projects.

+
+

These steps reference an SPFx solution, but apply to any solution.

+

Using a config file

+

Within the src directory create a new file named pnpjs-config.ts and copy in the below content.

+
import { WebPartContext } from "@microsoft/sp-webpart-base";
+
+// import pnp, pnp logging system, and any other selective imports needed
+import { spfi, SPFI, SPFx } from "@pnp/sp";
+import { LogLevel, PnPLogging } from "@pnp/logging";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/batching";
+
+var _sp: SPFI = null;
+
+export const getSP = (context?: WebPartContext): SPFI => {
+  if (context != null) {
+    //You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency
+    // The LogLevel set's at what level a message will be written to the console
+    _sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));
+  }
+  return _sp;
+};
+
+

To initialize the configuration, from the onInit function (or whatever function runs first in your code) make a call to getSP passing in the SPFx context object (or whatever configuration you would require for your setup).

+
protected async onInit(): Promise<void> {
+  this._environmentMessage = this._getEnvironmentMessage();
+
+  super.onInit();
+
+  //Initialize our _sp object that we can then use in other packages without having to pass around the context.
+  //  Check out pnpjsConfig.ts for an example of a project setup file.
+  getSP(this.context);
+}
+
+

Now you can consume your configured _sp object from anywhere else in your code by simply referencing the pnpjs-presets.ts file via an import statement and then getting a local instance of the _sp object using the getSP() method without passing any context.

+
import { getSP } from './pnpjs-config.ts';
+...
+export default class PnPjsExample extends React.Component<IPnPjsExampleProps, IIPnPjsExampleState> {
+
+  private _sp: SPFI;
+
+  constructor(props: IPnPjsExampleProps) {
+    super(props);
+    // set initial state
+    this.state = {
+      items: [],
+      errors: []
+    };
+    this._sp = getSP();
+  }
+
+  ...
+
+}
+
+

Use a service class

+

Because you do not have full access to the context object within a service you need to setup things a little differently.

+
import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
+import { PageContext } from "@microsoft/sp-page-context";
+import { AadTokenProviderFactory } from "@microsoft/sp-http";
+import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
+import { graphfi, GraphFI, SPFx as gSPFx } from "@pnp/graph";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+export interface ISampleService {
+    getLists(): Promise<any[]>;
+}
+
+export class SampleService {
+
+    public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService);
+    private _sp: SPFI;
+    private _graph: GraphFI;
+
+    constructor(serviceScope: ServiceScope) {
+
+        serviceScope.whenFinished(() => {
+
+        const pageContext = serviceScope.consume(PageContext.serviceKey);
+        const aadTokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);
+
+        //SharePoint
+        this._sp = spfi().using(spSPFx({ pageContext }));
+
+        //Graph
+        this._graph = graphfi().using(gSPFx({ aadTokenProviderFactory }));
+    }
+
+    public getLists(): Promise<any[]> {
+        return this._sp.web.lists();
+    }
+}
+
+

Depending on the architecture of your solution you can also opt to export the service as a global. If you choose this route you would need to modify the service to create an Init function where you would pass the service scope instead of doing so in the constructor. You would then export a constant that creates a global instance of the service.

+
export const mySampleService = new SampleService();
+
+

For a full sample, please see our PnPjs Version 3 Sample Project

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/selective-imports/index.html b/concepts/selective-imports/index.html new file mode 100644 index 000000000..3d8206e02 --- /dev/null +++ b/concepts/selective-imports/index.html @@ -0,0 +1,2771 @@ + + + + + + + + + + + + + + + + + + + + + + + + Selective Imports - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Selective Imports

+

As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking.

+

This concept works well with custom bundling to create a shared package tailored exactly to your needs.

+

If you would prefer to not worry about selective imports please see the section on presets.

+
+

A quick note on how TypeScript handles type only imports. If you have a line like import { IWeb } from "@pnp/sp/webs" everything will transpile correctly but you will get runtime errors because TS will see that line as a type only import and drop it. You need to include both import { IWeb } from "@pnp/sp/webs" and import "@pnp/sp/webs" to ensure the webs functionality is correctly included. You can see this in the last example below.

+
+
// the sp var now has almost nothing attached at import time and relies on
+
+// we need to import each of the pieces we need to "attach" them for chaining
+// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items/list";
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
+
+

Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific

+
// the sp var now has almost nothing attached at import time and relies on
+
+// we need to import each of the pieces we need to "attach" them for chaining
+// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
+
+

The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example:

+
// this import statement will attach content-type functionality to list, web, and item
+import "@pnp/sp/content-types";
+
+// this import statement will only attach content-type functionality to web
+import "@pnp/sp/content-types/web";
+
+

If you only need to access content types on the web object you can reduce size by only importing that piece.

+

The below example shows the need to import types and module augmentation separately.

+
// this will fail
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists";
+
+// do this instead
+import { sp } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import { IList } from "@pnp/sp/lists";
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+const lists = await sp.web.lists();
+
+

Presets

+

Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually. Both libraries supply an "all" preset that will attach all of the available library functionality.

+
+

While the presets provided may be useful, we encourage you to look at making your own project presets or custom bundles as a preferred solution. Use of the presets in client-side solutions is not recommended.

+
+

SP

+
import "@pnp/sp/presets/all";
+
+
+// placeholder for fully configuring the sp interface
+const sp = spfi();
+
+// sp.* will have all of the library functionality bound to it, tree shaking will not work
+const lists = await sp.web.lists();
+
+

Graph

+

The graph library contains a single preset, "all" mimicking the v1 structure.

+
import "@pnp/graph/presets/all";
+import { graphfi } from "@pnp/graph";
+
+// placeholder for fully configuring the sp interface
+const graph = graphfi();
+
+// graph.* will have all of the library functionality bound to it, tree shaking will not work
+const me = await graph.me();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/concepts/typings/index.html b/concepts/typings/index.html new file mode 100644 index 000000000..bcb55f946 --- /dev/null +++ b/concepts/typings/index.html @@ -0,0 +1,2630 @@ + + + + + + + + + + + + + + + + + + + + + + + + Typings - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Typing Return Objects

+

Whenever you make a request of the library for data from an object and utilize the select method to reduce the size of the objects in the payload its preferable in TypeScript to be able to type that returned object. The library provides you a method to do so by using TypeScript's Generics declaration.

+

By defining the objects type in the <> after the closure of the select method the resulting object is typed.

+
  .select("Title")<{Title: string}>()
+
+

Below are some examples of typing the return payload:

+
  const _sp = spfi().using(SPFx(this.context));
+
+  //Typing the Title property of a field
+  const field = await _sp.site.rootWeb.fields.getById(titleFieldId).select("Title")<{ Title: string }>();
+
+  //Typing the ParentWebUrl property of the selected list.
+  const testList = await _sp.web.lists.getByTitle('MyList').select("ParentWebUrl")<{ ParentWebUrl: string }>();
+
+
+

There have been discussions in the past around auto-typing based on select and the expected properties of the return object. We haven't done so for a few reasons: there is no even mildly complex way to account for all the possibilities expand introduces to selects, and if we "ignore" expand it effectively makes the select typings back to "any". Looking at template types etc, we haven't yet seen a way to do this that makes it worth the effort and doesn't introduce some other limitation or confusion.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/debug-tests/index.html b/contributing/debug-tests/index.html new file mode 100644 index 000000000..5cc109e1c --- /dev/null +++ b/contributing/debug-tests/index.html @@ -0,0 +1,2756 @@ + + + + + + + + + + + + + + + + + + + + + + + + Tests - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Writing Tests

+

With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.

+

How to write Tests

+

We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts:

+
import { getRandomString } from "@pnp/core";
+import { testSettings } from "../main";
+import { expect } from "chai";
+import { sp } from "@pnp/sp";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items/list";
+import { IList } from "@pnp/sp/lists";
+
+describe("Items", () => {
+
+    // any tests that make a web request should be withing a block checking if web tests are enabled
+    if (testSettings.enableWebTests) {
+
+        // a block scoped var we will use across our tests
+        let list: IList = null;
+
+        // we use the before block to setup
+        // executed before all the tests in this block, see the mocha docs for more details
+        // mocha prefers using function vs arrow functions and this is recommended
+        before(async function () {
+
+            // execute a request to ensure we have a list
+            const ler = await sp.web.lists.ensure("ItemTestList", "Used to test item operations");
+            list = ler.list;
+
+            // in this case we want to have some items in the list for testing so we add those
+            // only if the list was just created
+            if (ler.created) {
+
+                // add a few items to get started
+                const batch = sp.web.createBatch();
+                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
+                await batch.execute();
+            }
+        });
+
+        // this test has a label "get items" and is run via an async function
+        it("get items", async function () {
+
+            // make a request for the list's items
+            const items = await list.items();
+
+            // report that we expect that result to be an array with more than 0 items
+            expect(items.length).to.be.gt(0);
+        });
+
+        // ... remainder of code removed
+    }
+}
+
+

General Guidelines for Writing Tests

+
    +
  • Tests should operate within the site defined in testSettings
  • +
  • Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves
  • +
  • Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll
  • +
  • When writing tests you can use "only" and "skip" from mochajs to focus on only the tests you are writing
  • +
  • Be sure to review the various options when running your tests
  • +
  • If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description
  • +
+

Next Steps

+

Now that you've written tests to cover your changes you'll need to update the docs.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/debugging/index.html b/contributing/debugging/index.html new file mode 100644 index 000000000..7e62a0fed --- /dev/null +++ b/contributing/debugging/index.html @@ -0,0 +1,2947 @@ + + + + + + + + + + + + + + + + + + + + + + + + Debugging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Debugging

+

Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on.

+

Before proceeding be sure you have reviewed how to setup for local configuration and debugging.

+

Debugging Library Features

+

The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.

+

Basic SharePoint Testing

+

You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules.

+

All of the setup for the node client is handled within sp.ts using the settings from the local configuration.

+

Basic Graph Testing

+

Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit.

+

All of the setup for the node client is handled within graph.ts using the settings from the local configuration.

+

How to: Create a Debug Module

+

If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git.

+

Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content:

+
// note we can use the actual package names for our imports (ex: @pnp/logging)
+import { Logger, LogLevel, ConsoleListener } from "@pnp/logging";
+// using the all preset for simplicity in the example, selective imports work as expected
+import { sp, ListEnsureResult } from "@pnp/sp/presets/all";
+
+declare var process: { exit(code?: number): void };
+
+export async function MyDebug() {
+
+  // configure your options
+  // you can have different configs in different modules as needed for your testing/dev work
+  sp.setup({
+    sp: {
+      fetchClientFactory: () => {
+        return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret);
+      },
+    },
+  });
+
+  // run some debugging
+  const list = await sp.web.lists.ensure("MyFirstList");
+
+  Logger.log({
+    data: list.created,
+    level: LogLevel.Info,
+    message: "Was list created?",
+  });
+
+  if (list.created) {
+
+    Logger.log({
+      data: list.data,
+      level: LogLevel.Info,
+      message: "Raw data from list creation.",
+    });
+
+  } else {
+
+    Logger.log({
+      data: null,
+      level: LogLevel.Info,
+      message: "List already existed!",
+    });
+  }
+
+  process.exit(0);
+}
+
+

Update main.ts to launch your module

+

First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this:

+
// ...
+
+// comment out the example
+// import { Example } from "./example";
+// Example();
+
+import { MyDebug } from "./mydebug"
+MyDebug();
+
+// ...
+
+
+

Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)

+
+

Debug

+

Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.

+

Debug Module Next Steps

+

Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.

+

In Browser Debugging

+

You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.

+

Start the local serve

+

This will serve a package with ./debug/serve/main.ts as the entry.

+

npm run serve

+

Add reference to library

+

Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.

+
<script src="https://localhost:8080/assets/pnp.js"></script>
+<div id="pnp-test"></div>
+
+

You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.

+

Debug

+

Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.

+

Next Steps

+

You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser.

+

Now you can learn about extending the library.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/documentation/index.html b/contributing/documentation/index.html new file mode 100644 index 000000000..325bcbd17 --- /dev/null +++ b/contributing/documentation/index.html @@ -0,0 +1,2724 @@ + + + + + + + + + + + + + + + + + + + + + + + + Documentation - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Documentation

+

Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.

+

Writing Docs

+

Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.

+

Building Docs Locally

+

Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.

+

When executing the pip module on Windows you can prefix it with python -m. +For example:

+

python -m pip install mkdocs-material

+
    +
  • Install MkDocs
      +
    • pip install mkdocs
    • +
    +
  • +
  • Install the Material theme
      +
    • pip install mkdocs-material
    • +
    +
  • +
  • install the mkdocs-markdownextradata-plugin - this is used for the version variable
      +
    • pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7)
    • +
    +
  • +
  • install redirect plugin - used to redirect from moved pages
      +
    • pip install mkdocs-redirects
    • +
    +
  • +
  • Serve it up
      +
    • mkdocs serve
    • +
    • Open a browser to http://127.0.0.1:8000/
    • +
    +
  • +
+
+

Please see the official mkdocs site for more details on working with mkdocs

+
+

Next Steps

+

After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request!

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/extending-the-library/index.html b/contributing/extending-the-library/index.html new file mode 100644 index 000000000..9f3373d6e --- /dev/null +++ b/contributing/extending-the-library/index.html @@ -0,0 +1,2955 @@ + + + + + + + + + + + + + + + + + + + + + + + + Expanding the Library - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

Extending PnPjs

+
+

This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property.

+
+

At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the "webs" property is added to the web class.

+
// TypeScript property, returning an interface
+public get webs(): IWebs {
+    // using the Webs factory function and providing "this" as the first parameter
+    return Webs(this);
+}
+
+

Understanding Factory Functions

+

PnPjs v3 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form.

+
// create a constant which is a function of type ISPInvokableFactory having the name Webs
+// this is bound by the generic type param to return an IWebs instance
+// and it will use the _Webs concrete class to form the internal type of the invocable
+export const Webs = spInvokableFactory<IWebs>(_Webs);
+
+

The ISPInvokableFactory type looks like:

+
export type ISPInvokableFactory<R = any> = (baseUrl: string | ISharePointQueryable, path?: string) => R;
+
+

And the matching graph type:

+
<R>(f: any): (baseUrl: string | IGraphQueryable, path?: string) => R
+
+

The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples.

+
import { SPFx } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+// create a web from an absolute url
+const web = Web("https://tenant.sharepoint.com").using(SPFx(this.context));
+
+// as an example, create a new web using the first as a base
+// targets: https://tenant.sharepoint.com/sites/dev
+const web2 = Web(web, "sites/dev");
+
+// or you can add any path components you want, here as an example we access the current user property
+const cu = Web(web, "currentuser");
+const currentUserInfo = cu();
+
+

Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their "type". It is an important concept when working with the library to always remember we are just building strings.

+

Class structure

+

Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method

+
/*
+The concrete class implementation. This is never exported or shown directly
+to consumers of the library. It is wrapped by the Proxy we do expose.
+
+It extends the _SharePointQueryableInstance class for which there is a matching
+_SharePointQueryableCollection. The generic parameter defines the return type
+of a get operation and the invoked result.
+
+Classes can have methods and properties as normal. This one has a single property as a simple example
+*/
+export class _HubSite extends _SharePointQueryableInstance<IHubSiteInfo> {
+
+    /**
+     * Gets the ISite instance associated with this hub site
+     */
+    // the tag decorator is used to provide some additional telemetry on what methods are
+    // being called.
+    @tag("hs.getSite")
+    public async getSite(): Promise<ISite> {
+
+        // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result
+        const d = await this.select("SiteUrl")();
+
+        // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl
+        return Site(d.SiteUrl);
+    }
+}
+
+/*
+This defines the interface we export and expose to consumers.
+In most cases this extends the concrete object but may add or remove some methods/properties
+in special cases
+*/
+export interface IHubSite extends _HubSite { }
+
+/*
+This defines the HubSite factory function as discussed above
+binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite.
+
+This is understood to mean that HubSite is a factory function that returns a types of IHubSite
+which the spInvokableFactory will create using _HubSite as the concrete underlying type.
+*/
+export const HubSite = spInvokableFactory<IHubSite>(_HubSite);
+
+

Add a Property

+

In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class.

+
export class _View extends _SharePointQueryableInstance<IViewInfo> {
+
+    // ... other code removed
+
+    // add the property, and provide a return type
+    // return types should be interfaces
+    public get fields(): IViewFields {
+        // we use the ViewFields factory function supplying "this" as the first parameter
+        // this will create a url like ".../fields/viewfields" due to the defaultPath decorator
+        // on the _ViewFields class. This is equivalent to: ViewFields(this, "viewfields")
+        return ViewFields(this);
+    }
+
+    // ... other code removed
+}
+
+
+

There are many examples throughout the library that follow this pattern.

+
+

Add a Method

+

Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method:

+
@defaultPath("items")
+export class _Items extends _SharePointQueryableCollection {
+
+    /**
+    * Gets an Item by id
+    *
+    * @param id The integer id of the item to retrieve
+    */
+    // we declare a method and set the return type to an interface
+    public getById(id: number): IItem {
+        // here we use the tag helper to add some telemetry to our request
+        // we create a new IItem using the factory and appending the id value to the end
+        // this gives us a valid url path to a single item .../items/getById(2)
+        // we can then use the returned IItem to extend our chain or execute a request
+        return tag.configure(Item(this).concat(`(${id})`), "is.getById");
+    }
+
+    // ... other code removed
+}
+
+

Web Request Method

+

A second example is a method that performs a request. Here we use the _Item recycle method as an example:

+
/**
+ * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item.
+ */
+// we use the tag decorator to add telemetry
+@tag("i.recycle")
+// we return a promise
+public recycle(): Promise<string> {
+    // we use the spPost method to post the request created by cloning our current instance IItem using
+    // the Item factory and adding the path "recycle" to the end. Url will look like .../items/getById(2)/recycle
+    return spPost<string>(Item(this, "recycle"));
+}
+
+

Augment Using Selective Imports

+

To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available.

+
// import the addProp helper
+import { addProp } from "@pnp/queryable";
+// import the _List concrete class from the types module (not the index!)
+import { _List } from "../lists/types";
+// import the interface and factory we are going to add to the List
+import { Items, IItems } from "./types";
+
+// This module declaration fixes up the types, allowing .items to appear in intellisense
+// when you import "@pnp/sp/items/list";
+declare module "../lists/types" {
+    // we need to extend the concrete type
+    interface _List {
+        readonly items: IItems;
+    }
+    // we need to extend the interface
+    // this may not be strictly necessary as the IList interface extends _List so it
+    // should pick up the same additions, but we have seen in some cases this does seem
+    // to be required. So we include it for safety as it will all be removed during
+    // transpilation we don't need to care about the extra code
+    interface IList {
+        readonly items: IItems;
+    }
+}
+
+// finally we add the property to the _List class
+// this method call says add a property to _List named "items" and that property returns a result using the Items factory
+// The factory will be called with "this" when the property is accessed. If needed there is a fourth parameter to append additional path
+// information to the property url
+addProp(_List, "items", Items);
+
+

General Rules for Extending PnPjs

+
    +
  • Only expose interfaces to consumers
  • +
  • Use the factory functions except in very special cases
  • +
  • Look for other properties and methods as examples
  • +
  • Simple is always preferable, but not always possible - use your best judgement
  • +
  • If you find yourself writing a ton of code to solve a problem you think should be easy, ask
  • +
  • If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed
  • +
+

Next Steps

+

Now that you have extended the library you need to write a test to cover it!

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/index.html b/contributing/index.html new file mode 100644 index 000000000..0e19f8329 --- /dev/null +++ b/contributing/index.html @@ -0,0 +1,2707 @@ + + + + + + + + + + + + + + + + + + + + + + + + Contributing - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Contributing to PnPjs

+

Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SectionDescription
NPM ScriptsExplains the npm scripts and their uses
Setup Dev MachineCovers setting up your machine to ensure you are ready to debug the solution
Local Debug ConfigurationDiscusses the steps required to establish local configuration used for debugging and running tests
DebuggingDescribes how to debug PnPjs locally
Extending the libraryBasic examples on how to extend the library such as adding a method or property
Writing TestsHow to write and debug tests
Update DocumentationDescribes the steps required to edit and locally view the documentation
Submit a Pull RequestOutlines guidance for submitting a pull request
+

Need Help?

+

The PnP "Sharing Is Caring" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.

+

Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.

+

To learn more and register for an upcoming session, please visit the Sharing is Caring website.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/local-debug-configuration/index.html b/contributing/local-debug-configuration/index.html new file mode 100644 index 000000000..429a19714 --- /dev/null +++ b/contributing/local-debug-configuration/index.html @@ -0,0 +1,2717 @@ + + + + + + + + + + + + + + + + + + + + + + + + Local Debug Config - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Local Debugging Configuration

+

This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).

+

Create settings.js

+

Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. +For more information the settings file please see Settings

+

Minimal Configuration

+

You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag.

+

The following configuration file allows you to run all the tests that do not contact services.

+
 var sets = {
+     testing: {
+         enableWebTests: false,
+     }
+ }
+
+module.exports = sets;
+
+

Test your setup

+

If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/npm-scripts/index.html b/contributing/npm-scripts/index.html new file mode 100644 index 000000000..0724883ea --- /dev/null +++ b/contributing/npm-scripts/index.html @@ -0,0 +1,2958 @@ + + + + + + + + + + + + + + + + + + + + + + + + Npm Scripts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Supported NPM Scripts

+

As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies.

+

This article outlines the current scripts we've implemented and how to use them, with available options and examples.

+

Start

+

Executes the serve command

+
npm start
+
+

Serve

+

Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node.

+
npm run serve
+
+

Test

+

Runs the tests and coverage for the library.

+

More details on setting up MSAL for node.

+

Options

+

There are several options you can provide to the test command. All of these need to be separated using a "--" double hyphen so they are passed to the spawned sub-commands.

+

Test a Single Package

+
+

--package or -p

+
+

This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory.

+
# run only sp tests
+npm test -- -p sp
+
+# run only logging tests
+npm test -- -package logging
+
+

Run a Single Test File

+
+

--single or --s

+
+

You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags.

+
# run only sp web tests
+npm test -- -p sp -s web
+
+# run only graph groups tests
+npm test -- -package graph -single groups
+
+

Specify a Site

+
+

--site

+
+

By default every time you run the tests a new sub-site is created below the site specified in your settings file. You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option.

+

This option can be used with any or none of the other testing options.

+
# run only sp web tests with a certain site
+npm test -- -p sp -s web --site https://some.site.com/sites/dev
+
+

Cleanup

+
+

--cleanup

+
+

If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted.

+
# clean up our testing site
+npm test -- --cleanup
+
+

Logging

+
+

--logging

+
+

If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output.

+
# enable logging during testing
+npm test -- --logging
+
+

You can also optionally set a log level of error, warning, info, or verbose:

+
# enable logging during testing in verbose (lots of info)
+npm test -- --logging verbose
+
+
# enable logging during testing in error
+npm test -- --logging error
+
+

spVerbose

+
+

--spverbose

+
+

This flag will enable "verbose" OData mode for SharePoint tests. This flag is compatible with other flags.

+
npm test -- --spverbose
+
+

build

+

Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed.

+
npm run build
+
+

package

+

Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published.

+
npm run package
+
+

lint

+

Runs the linter.

+
npm run lint
+
+

clean

+

Removes any generated folders from the working directory.

+
npm run clean
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/pull-requests/index.html b/contributing/pull-requests/index.html new file mode 100644 index 000000000..16c8042bb --- /dev/null +++ b/contributing/pull-requests/index.html @@ -0,0 +1,2694 @@ + + + + + + + + + + + + + + + + + + + + + + + + Pull Requests - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Submitting Pull Requests

+

Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release.

+
    +
  • Target your pull requests to the version-3 branch
  • +
  • Add/Update any relevant docs articles in the relevant package's docs folder related to your changes
  • +
  • Include a test for any new functionality and ensure all existing tests are passing by running npm test
  • +
  • Ensure linting checks pass by typing npm run lint
  • +
  • Ensure everything works for a build by running npm run package
  • +
  • Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work
  • +
  • If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :)
  • +
+
+

If you need to target a PR for version 1, please target the "version-1" branch

+
+

Sharing is Caring - Pull Request Guidance

+

The PnP "Sharing Is Caring" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.

+

Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.

+

To learn more and register for an upcoming session, please visit the Sharing is Caring website.

+

Next Steps

+

Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted.

+

Thank you for helping PnPjs grow and improve!!

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/settings/index.html b/contributing/settings/index.html new file mode 100644 index 000000000..10fcd866b --- /dev/null +++ b/contributing/settings/index.html @@ -0,0 +1,2848 @@ + + + + + + + + + + + + + + + + + + + + + + + + Settings - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Project Settings

+

This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally.

+

The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root.

+

Settings File Format

+

The settings file is configured with MSAL authentication for both SharePoint and Graph. For more information coinfiguring MSAL please review the section in the authentication section for node.

+

MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always "https://{tenant}.sharepoint.com/.default" or "https://graph.microsoft.com/.default" depending on what you are calling.

+
+

If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated.

+
+

You will need to create testing certs for the sample settings file below. Using the following code you end up with three files, "cert.pem", "key.pem", and "keytmp.pem". The "cert.pem" file is uploaded to your AAD application registration. The "key.pem" is read as the private key for the configuration. Copy the contents of the "key.pem" file and paste it in the privateKey variable below. The gitignore file in this repository will ignore the settings.js file.

+
+

Replace HereIsMySuperPass with your own password

+
+
mkdir \temp
+cd \temp
+openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'
+openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass
+
+
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
+your private key, read from a file or included here
+-----END RSA PRIVATE KEY-----
+`;
+
+var msalInit = {
+    auth: {
+        authority: "https://login.microsoftonline.com/{tenant id}",
+        clientCertificate: {
+            thumbprint: "{certificate thumbnail}",
+            privateKey: privateKey,
+        },
+        clientId: "{AAD App registration id}",
+    }
+}
+
+export const settings = {
+    testing: {
+        enableWebTests: true,
+        testUser: "i:0#.f|membership|user@consto.com",
+        testGroupId:"{ Microsoft 365 Group ID }",
+        sp: {
+            url: "{required for MSAL - absolute url of test site}",
+            notificationUrl: "{ optional: notification url }",
+            msal: {
+                init: msalInit,
+                scopes: ["https://{tenant}.sharepoint.com/.default"]
+            },
+        },
+        graph: {
+            msal: {
+                init: msalInit,
+                scopes: ["https://graph.microsoft.com/.default"]
+            },
+        },
+    },
+}
+
+
+

The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
enableWebTestsFlag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required.
testUserAAD login account to be used when running tests.
testGroupIdGroup ID of Microsoft 365 Group to be used when running test cases.
spSettings used to configure SharePoint (sp library) debugging and tests
graphSettings used to configure Microsoft Graph (graph library) debugging and tests
+

SP values

+ + + + + + + + + + + + + + + + + + + + + +
namedescription
urlThe url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details.
notificationUrlUrl used when registering test subscriptions
msalInformation about MSAL authentication setup
+

Graph value

+

The graph values are described in the table below and come from registering an AAD Application. The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against.

+ + + + + + + + + + + + + +
namedescription
msalInformation about MSAL authentication setup
+

Create Settings.js file

+
    +
  1. Copy the example file and rename it settings.js. Place the file in the root of your project.
  2. +
  3. Update the settings as needed for your environment.
  4. +
+
+

If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/contributing/setup-dev-machine/index.html b/contributing/setup-dev-machine/index.html new file mode 100644 index 000000000..361261574 --- /dev/null +++ b/contributing/setup-dev-machine/index.html @@ -0,0 +1,2717 @@ + + + + + + + + + + + + + + + + + + + + + + + + Set Up Dev Machine - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Setting up your Developer Machine

+

If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging.

+

Setup your development environment

+

These steps will help you get your environment setup for contributing to the core library.

+
    +
  1. +

    Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like.

    +
  2. +
  3. +

    Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).

    +
    +

    This library requires node >= 10.18.0

    +
    +
  4. +
  5. +

    On Windows: Install Python

    +
  6. +
  7. +

    [Optional] Install the tslint extension in VS Code:

    +
      +
    1. Press Shift + Ctrl + "p" to open the command panel
    2. +
    3. Begin typing "install extension" and select the command when it appears in view
    4. +
    5. Begin typing "tslint" and select the package when it appears in view
    6. +
    7. Restart Code after installation
    8. +
    +
  8. +
+

Fork The Repo

+

All of our contributions come via pull requests and you'll need to fork the repository

+
    +
  1. +

    Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.

    +
  2. +
  3. +

    Once you have the code locally, navigate to the root of the project in your console. Type the following command:

    +

    npm install

    +
  4. +
  5. +

    Follow the guidance to complete the one-time local configuration required to debug and run tests.

    +
  6. +
  7. +

    Then you can follow the guidance in the debugging article.

    +
  8. +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/behavior-recipes/index.html b/core/behavior-recipes/index.html new file mode 100644 index 000000000..74ee7b017 --- /dev/null +++ b/core/behavior-recipes/index.html @@ -0,0 +1,2869 @@ + + + + + + + + + + + + + + + + + + + + + + + + Behavior Recipes - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Behavior Recipes

+

This article contains example recipes for building your own behaviors. We don't want to include every possible behavior within the library, but do want folks to have easy ways to solve the problems they encounter. If have ideas for a missing recipe, please let us know in the issues list OR submit them to this page as a PR! We want to see what types of behaviors folks build and will evaluate options to either include them in the main libraries, leave them here as a reference resource, or possibly release a community behaviors package.

+
+

Alternatively we encourage you to publish your own behaviors as npm packages to share with others!

+
+

Proxy

+

At times you might need to introduce a proxy for requests for debugging or other networking needs. You can easily do so using your proxy of choice in Nodejs. This example uses "https-proxy-agent" but would work similarly for any implementation.

+

proxy.ts

+
import { TimelinePipe } from "@pnp/core";
+import { Queryable } from "@pnp/queryable";
+import { HttpsProxyAgent } from "https-proxy-agent";
+
+export function Proxy(proxyInit: string): TimelinePipe<Queryable>;
+// eslint-disable-next-line no-redeclare
+export function Proxy(proxyInit: any): TimelinePipe<Queryable>;
+// eslint-disable-next-line no-redeclare
+export function Proxy(proxyInit: any): TimelinePipe<Queryable> {
+
+    const proxy = typeof proxyInit === "string" ? new HttpsProxyAgent(proxyInit) : proxyInit;
+
+    return (instance: Queryable) => {
+
+        instance.on.pre(async (url, init, result) => {
+
+            // we add the proxy to the request
+            (<any>init).agent = proxy;
+
+            return [url, init, result];
+        });
+
+        return instance;
+    };
+}
+
+

usage

+
import { Proxy } from "./proxy.ts";
+
+import "@pnp/sp/webs";
+import { SPDefault } from "@pnp/nodejs";
+
+// would work with graph library in the same manner
+const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault({
+    msal: {
+        config: { config },
+        scopes: {scopes },
+    },
+}), Proxy("http://127.0.0.1:8888"));
+
+const webInfo = await sp.webs();
+
+

Add QueryString to bypass request caching

+

In some instances users express a desire to append something to the querystring to avoid getting cached responses back for requests. This pattern is an example of doing that in v3.

+

query-cache-param.ts

+
export function CacheBust(): TimelinePipe<Queryable> {
+
+    return (instance: Queryable) => {
+
+        instance.on.pre(async (url, init, result) => {
+
+            url += url.indexOf("?") > -1 ? "&" : "?";
+
+            url += "nonce=" + encodeURIComponent(new Date().toISOString());
+
+            return [url, init, result];
+        });
+
+        return instance;
+    };
+}
+
+

usage

+
import { CacheBust } from "./query-cache-param.ts";
+
+import "@pnp/sp/webs";
+import { SPDefault } from "@pnp/nodejs";
+
+// would work with graph library in the same manner
+const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault({
+    msal: {
+        config: { config },
+        scopes: { scopes },
+    },
+}), CacheBust());
+
+const webInfo = await sp.webs();
+
+

ACS Authentication

+

Starting with v3 we no longer provide support for ACS authentication within the library. However you may have a need (legacy applications, on-premises) to use ACS authentication while wanting to migrate to v3. Below you can find an example implementation of an Authentication observer for ACS. This is not a 100% full implementation, for example the tokens are not cached.

+
+

Whenever possible we encourage you to use AAD authentication and move away from ACS for securing your server-side applications.

+
+
export function ACS(clientId: string, clientSecret: string, authUrl = "https://accounts.accesscontrol.windows.net"): (instance: Queryable) => Queryable {
+
+  const SharePointServicePrincipal = "00000003-0000-0ff1-ce00-000000000000";
+
+  async function getRealm(siteUrl: string): Promise<string> {
+
+    const url = combine(siteUrl, "_vti_bin/client.svc");
+
+    const r = await nodeFetch(url, {
+      "headers": {
+        "Authorization": "Bearer ",
+      },
+      "method": "POST",
+    });
+
+    const data: string = r.headers.get("www-authenticate") || "";
+    const index = data.indexOf("Bearer realm=\"");
+    return data.substring(index + 14, index + 50);
+  }
+
+  function getFormattedPrincipal(principalName: string, hostName: string, realm: string): string {
+    let resource = principalName;
+    if (hostName !== null && hostName !== "") {
+      resource += "/" + hostName;
+    }
+    resource += "@" + realm;
+    return resource;
+  }
+
+  async function getFullAuthUrl(realm: string): Promise<string> {
+
+    const url = combine(authUrl, `/metadata/json/1?realm=${realm}`);
+
+    const r = await nodeFetch(url, { method: "GET" });
+    const json: { endpoints: { protocol: string; location: string }[] } = await r.json();
+
+    const eps = json.endpoints.filter(ep => ep.protocol === "OAuth2");
+    if (eps.length > 0) {
+      return eps[0].location;
+    }
+
+    throw Error("Auth URL Endpoint could not be determined from data.");
+  }
+
+  return (instance: Queryable) => {
+
+    instance.on.auth.replace(async (url: URL, init: RequestInit) => {
+
+      const realm = await getRealm(url.toString());
+      const fullAuthUrl = await getFullAuthUrl(realm);
+
+      const resource = getFormattedPrincipal(SharePointServicePrincipal, url.host, realm);
+      const formattedClientId = getFormattedPrincipal(clientId, "", realm);
+
+      const body: string[] = [];
+      body.push("grant_type=client_credentials");
+      body.push(`client_id=${formattedClientId}`);
+      body.push(`client_secret=${encodeURIComponent(clientSecret)}`);
+      body.push(`resource=${resource}`);
+
+      const r = await nodeFetch(fullAuthUrl, {
+        body: body.join("&"),
+        headers: {
+          "Content-Type": "application/x-www-form-urlencoded",
+        },
+        method: "POST",
+      });
+
+      const accessToken: { access_token: string } = await r.json();
+
+      init.headers = { ...init.headers, Authorization: `Bearer ${accessToken.access_token}` };
+
+      return [url, init];
+    });
+
+    return instance;
+  };
+}
+
+

usage

+
import { CacheBust } from "./acs-auth-behavior.ts";
+import "@pnp/sp/webs";
+import { SPDefault } from "@pnp/nodejs";
+
+const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault(), ACS("{client id}", "{client secret}"));
+
+// you can optionally provide the authentication url, here using the one for China's sovereign cloud or an local url if working on-premises
+// const sp = spfi("https://tenant.sharepoint.com/sites.dev").using(SPDefault(), ACS("{client id}", "{client secret}", "https://accounts.accesscontrol.chinacloudapi.cn"));
+
+const webInfo = await sp.webs();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/behaviors/index.html b/core/behaviors/index.html new file mode 100644 index 000000000..84074b1b6 --- /dev/null +++ b/core/behaviors/index.html @@ -0,0 +1,2882 @@ + + + + + + + + + + + + + + + + + + + + + + + + Behaviors - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/core : behaviors

+

While you can always register observers to any Timeline's moments using the .on.moment syntax, to make things easier we have included the ability to create behaviors. Behaviors define one or more observer registrations abstracted into a single registration. To differentiate behaviors are applied with the .using method. The power of behaviors is they are composable so a behavior can apply other behaviors.

+

Basic Example

+

Let's create a behavior that will register two observers to a Timeline. We'll use error and log since they exist on all Timelines. In this example let's imagine we need to include some special secret into every lifecycle for logging to work. And we also want a company wide method to track errors. So we roll our own behavior.

+
import { Timeline, TimelinePipe } from "@pnp/core";
+import { MySpecialLoggingFunction } from "../mylogging.js";
+
+// top level function allows binding of values within the closure
+export function MyBehavior(specialSecret: string): TimelinePipe {
+
+    // returns the actual behavior function that is applied to the instance
+    return (instance: Timeline<any>) => {
+
+        // register as many observers as needed
+        instance.on.log(function (message: string, severity: number) {
+
+            MySpecialLoggingFunction(message, severity, specialSecret);
+        });
+
+        instance.on.error(function (err: string | Error) {
+
+            MySpecialLoggingFunction(typeof err === "string" ? err : err.toString(), severity, specialSecret);
+        });
+
+        return instance;
+    };
+}
+
+// apply the behavior to a Timeline/Queryable
+obj.using(MyBehavior("HereIsMySuperSecretValue"));
+
+

Composing Behaviors

+

We encourage you to use our defaults, or create your own default behavior appropriate to your needs. You can see all of the behaviors available in @pnp/nodejs, @pnp/queryable, @pnp/sp, and @pnp/graph.

+

As an example, let's create our own behavior for a nodejs project. We want to call the graph, default to the beta endpoint, setup MSAL, and include a custom header we need for our environment. To do so we create a composed behavior consisting of graph's DefaultInit, graph's DefaultHeaders, nodejs's MSAL, nodejs's NodeFetchWithRetry, and queryable's DefaultParse & InjectHeaders. Then we can import this behavior into all our projects to configure them.

+

company-default.ts

+
import { TimelinePipe } from "@pnp/core";
+import { DefaultParse, Queryable, InjectHeaders } from "@pnp/queryable";
+import { DefaultHeaders, DefaultInit } from "@pnp/graph";
+import { NodeFetchWithRetry, MSAL } from "@pnp/nodejs";
+
+export function CompanyDefault(): TimelinePipe<Queryable> {
+
+    return (instance: Queryable) => {
+
+        instance.using(
+            // use the default headers
+            DefaultHeaders(),
+            // use the default init, but change the base url to beta
+            DefaultInit("https://graph.microsoft.com/beta"),
+            // use node-fetch with retry
+            NodeFetchWithRetry(),
+            // use the default parsing
+            DefaultParse(),
+            // inject our special header to all requests
+            InjectHeaders({
+                "X-SomeSpecialToken": "{THE SPECIAL TOKEN VALUE}",
+            }),
+            // setup node's MSAL with configuration from the environment (or any source)
+            MSAL(process.env.MSAL_CONFIG));
+
+        return instance;
+    };
+}
+
+

index.ts

+
import { CompanyDefault } from "./company-default.ts";
+import { graphfi } from "@pnp/graph";
+
+// we can consistently and easily setup our graph instance using a single behavior
+const graph = graphfi().using(CompanyDefault());
+
+
+

You can easily share your composed behaviors across your projects using library components in SPFx, a company CDN, or an npm package.

+
+

+

Core Behaviors

+

This section describes two behaviors provided by the @pnp/core library, AssignFrom and CopyFrom. Likely you won't often need them directly - they are used in some places internally - but they are made available should they prove useful.

+

AssignFrom

+

This behavior creates a ref to the supplied Timeline implementation's observers and resets the inheriting flag. This means that changes to the parent, here being the supplied Timeline, will begin affecting the target to which this behavior is applied.

+
import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { AssignFrom } from "@pnp/core";
+// some local project file
+import { MyCustomeBehavior } from "./behaviors.ts";
+
+const source = spfi().using(SPBrowser());
+
+const target = spfi().using(MyCustomeBehavior());
+
+// target will now hold a reference to the observers contained in source
+// changes to the subscribed observers in source will apply to target
+// anything that was added by "MyCustomeBehavior" will no longer be present
+target.using(AssignFrom(source.web));
+
+// you can always apply additional behaviors or register directly on the events
+// but once you modify target it will not longer ref source and changes to source will no longer apply
+target.using(SomeOtherBehavior());
+target.on.log(console.log);
+
+

CopyFrom

+

Similar to AssignFrom, this method creates a copy of all the observers on the source and applies them to the target. This can be done either as a replace or append operation using the second parameter. The default is "append".

+
    +
  • "replace" will first clear each source moment's registered observers then apply each in source-order via the on operation.
  • +
  • "append" will apply each source moment's registered observers in source-order via the on operation
  • +
+
+

By design CopyFrom does NOT include moments defined by symbol keys.

+
+
import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { CopyFrom } from "@pnp/core";
+// some local project file
+import { MyCustomeBehavior } from "./behaviors.ts";
+
+const source = spfi().using(SPBrowser());
+
+const target = spfi().using(MyCustomeBehavior());
+
+// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are maintained as the default behavior is to append
+target.using(CopyFrom(source.web));
+
+// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are removed
+target.using(CopyFrom(source.web, "replace"));
+
+// you can always apply additional behaviors or register directly on the events
+// with CopyFrom no reference to source is maintained
+target.using(SomeOtherBehavior());
+target.on.log(console.log);
+
+

As well CopyFrom supports a filter parameter if you only want to copy the observers from a subset of moments. This filter is a predicate function taking a single string key and returning true if the observers from that moment should be copied to the target.

+
import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { CopyFrom } from "@pnp/core";
+// some local project file
+import { MyCustomeBehavior } from "./behaviors.ts";
+
+const source = spfi().using(SPBrowser());
+
+const target = spfi().using(MyCustomeBehavior());
+
+// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are maintained as the default behavior is to append
+target.using(CopyFrom(source.web));
+
+// target will have the observers `auth` and `send` copied from source, but no reference to source. Changes to source's registered observers will not affect target.
+// any previously registered observers in target are removed
+target.using(CopyFrom(source.web, "replace", (k) => /(auth|send)/i.test(k)));
+
+// you can always apply additional behaviors or register directly on the events
+// with CopyFrom no reference to source is maintained
+target.using(SomeOtherBehavior());
+target.on.log(console.log);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/moments/index.html b/core/moments/index.html new file mode 100644 index 000000000..50b9b2288 --- /dev/null +++ b/core/moments/index.html @@ -0,0 +1,2944 @@ + + + + + + + + + + + + + + + + + + + + + + + + moments - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/core : moments

+

Moments are the name we use to describe the steps executed during a timeline lifecycle. They are defined on a plain object by a series of functions with the general form:

+
// the first argument is the set of observers subscribed to the given moment
+// the rest of the args vary by an interaction between moment and observer types and represent the args passed when emit is called for a given moment
+function (observers: any[], ...args: any[]): any;
+
+

Let's have a look at one of the included moment factory functions, which define how the moment interacts with its registered observers, and use it to understand a bit more on how things work. In this example we'll look at the broadcast moment, used to mimic a classic event where no return value is tracked, we just want to emit an event to all the subscribed observers.

+
// the broadcast factory function, returning the actual moment implementation function
+// The type T is used by the typings of Timeline to described the arguments passed in emit
+export function broadcast<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {
+
+    // this is the actual moment implementation, called each time a given moment occurs in the timeline
+    return function (observers: T[], ...args: any[]): void {
+
+        // we make a local ref of the observers
+        const obs = [...observers];
+
+        // we loop through sending the args to each observer
+        for (let i = 0; i < obs.length; i++) {
+
+            // note that within every moment and observer "this" will be the current timeline object
+            Reflect.apply(obs[i], this, args);
+        }
+    };
+}
+
+

Let's use broadcast in a couple examples to show how it works. You can also review the timeline article for a fuller example.

+
// our first type determines the type of the observers that will be regsitered to the moment "first"
+type Broadcast1ObserverType = (this: Timeline<any>, message: string) => void;
+
+// our second type determines the type of the observers that will be regsitered to the moment "second"
+type Broadcast2ObserverType = (this: Timeline<any>, value: number, value2: number) => void;
+
+const moments = {
+    first: broadcast<Broadcast1ObserverType>(),
+    second: broadcast<Broadcast2ObserverType>(),
+} as const;
+
+

Now that we have defined two moments we can update our Timeline implementing class to emit each as we desire, as covered in the timeline article. Let's focus on the relationship between the moment definition and the typings inherited by on and emit in Timeline.

+

Because we want observers of a given moment to understand what arguments they will get the typings of Timeline are setup to use the type defining the moment's observer across all operations. For example, using our moment "first" from above. Each moment can be subscribed by zero or more observers.

+
// our observer function matches the type of Broadcast1ObserverType and the intellisense will reflect that.
+// If you want to change the signature you need only do so in the type Broadcast1ObserverType and the change will update the on and emit typings as well
+// here we want to reference "this" inside our observer function (preferred)
+obj.on.first(function (this: Timeline<any>, message: string) {
+    // we use "this", which will be the current timeline and the default log method to emit a logging event
+    this.log(message, 0);
+});
+
+// we don't need to reference "this" so we use arrow notation
+obj.on.first((message: string) => {
+    console.log(message);
+});
+
+

Similarily for second our observers would match Broadcast2Observer.

+
obj.on.second(function (this: Timeline<any>, value: number, value2: number) {
+    // we use "this", which will be the current timeline and the default log method to emit a logging event
+    this.log(`got value1: ${value} value2: ${value2}`, 0);
+});
+
+obj.on.second((value: number, value2: number) => {
+    console.log(`got value1: ${value} value2: ${value2}`);
+});
+
+

Existing Moment Factories

+

You a already familiar with broadcast which passes the emited args to all subscribed observers, this section lists the existing built in moment factories:

+

broadcast

+

Creates a moment that passes the emited args to all subscribed observers. Takes a single type parameter defining the observer signature and always returns void. Is not async.

+
import { broadcast } from "@pnp/core";
+
+// can have any method signature you want that returns void, "this" will always be set
+type BroadcastObserver = (this: Timeline<any>, message: string) => void;
+
+const moments = {
+    example: broadcast<BroadcastObserver>(),
+} as const;
+
+obj.on.example(function (this: Timeline<any>, message: string) {
+    this.log(message, 0);
+});
+
+obj.emit.example("Hello");
+
+

asyncReduce

+

Creates a moment that executes each observer asynchronously, awaiting the result and passes the returned arguments as the arguments to the next observer. This is very much like the redux pattern taking the arguments as the state which each observer may modify then returning a new state.

+
import { asyncReduce } from "@pnp/core";
+
+// can have any method signature you want, so long as it is async and returns a tuple matching in order the arguments, "this" will always be set
+type AsyncReduceObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<[string, number]>;
+
+const moments = {
+    example: asyncReduce<AsyncReduceObserver>(),
+} as const;
+
+obj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {
+
+    this.log(message, 0);
+
+    // we can manipulate the values
+    arg2++;
+
+    // always return a tuple of the passed arguments, possibly modified
+    return [arg1, arg2];
+});
+
+obj.emit.example("Hello", 42);
+
+

request

+

Creates a moment where the first registered observer is used to asynchronously execute a request, returning a single result. If no result is returned (undefined) no further action is taken and the result will be undefined (i.e. additional observers are not used).

+

This is used by us to execute web requets, but would also serve to represent any async request such as a database read, file read, or provisioning step.

+
import { request } from "@pnp/core";
+
+// can have any method signature you want, "this" will always be set
+type RequestObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<string>;
+
+const moments = {
+    example: request<RequestObserver>(),
+} as const;
+
+obj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {
+
+    this.log(`Sending request: ${arg1}`, 0);
+
+    // request expects a single value result
+    return `result value ${arg2}`;
+});
+
+obj.emit.example("Hello", 42);
+
+

Additional Examples

+

waitall

+

Perhaps you have a situation where you would like to wait until all of the subscribed observers for a given moment complete, but they can run async in parallel.

+
export function waitall<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<void> {
+
+    // this is the actual moment implementation, called each time a given moment occurs in the timeline
+    return function (observers: T[], ...args: any[]): void {
+
+        // we make a local ref of the observers
+        const obs = [...observers];
+
+        const promises = [];
+
+        // we loop through sending the args to each observer
+        for (let i = 0; i < obs.length; i++) {
+
+            // note that within every moment and observer "this" will be the current timeline object
+            promises.push(Reflect.apply(obs[i], this, args));
+        }
+
+        return Promise.all(promises).then(() => void(0));
+    };
+}
+
+

first

+

Perhaps you would instead like to only get the result of the first observer to return.

+
export function first<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<any> {
+
+    // this is the actual moment implementation, called each time a given moment occurs in the timeline
+    return function (observers: T[], ...args: any[]): void {
+
+        // we make a local ref of the observers
+        const obs = [...observers];
+
+        const promises = [];
+
+        // we loop through sending the args to each observer
+        for (let i = 0; i < obs.length; i++) {
+
+            // note that within every moment and observer "this" will be the current timeline object
+            promises.push(Reflect.apply(obs[i], this, args));
+        }
+
+        return Promise.race(promises);
+    };
+}
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/observers/index.html b/core/observers/index.html new file mode 100644 index 000000000..cb49c2e42 --- /dev/null +++ b/core/observers/index.html @@ -0,0 +1,2843 @@ + + + + + + + + + + + + + + + + + + + + + + + + observers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/core : observers

+

Observers are used to implement all of the functionality within a Timeline's moments. Each moment defines the signature of observers you can register, and calling the observers is orchestrated by the implementation of the moment. A few facts about observers:

+
    +
  • All observers are functions
  • +
  • The "this" of an observer is always the Timeline implementation that emitted the moment
  • +
  • Do not handle non-recoverable errors in observers, let them throw and they will be handled by the library appropriately and routed to the error moment.
  • +
+
+

For details on implementing observers for Queryable, please see this article.

+
+

Observer Inheritance

+

Timelines created from other timelines (i.e. how sp and graph libraries work) inherit all of the observers from the parent. Observers added to the parent will apply for all children.

+

When you make a change to the set of observers through any of the subscription methods outlined below that inheritance is broken. Meaning changes to the parent will no longer apply to that child, and changes to a child never affect a parent. This applies to ALL moments on change of ANY moment, there is no per-moment inheritance concept.

+
const sp = new spfi().using(...lots of behaviors);
+
+// web is current inheriting all observers from "sp"
+const web = sp.web;
+
+// at this point web no longer inherits from "sp" and has its own observers
+// but still includes everything that was registered in sp before this call
+web.on.log(...);
+
+// web2 inherits from sp as each invocation of .web creates a fresh IWeb instance
+const web2 = sp.web;
+
+// list inherits from web's observers and will contain the extra `log` observer added above
+const list = web.lists.getById("");
+
+// this new behavior will apply to web2 and any subsequent objects created from sp
+sp.using(AnotherBehavior());
+
+// web will again inherit from sp through web2, the extra log handler is gone
+// list now ALSO is reinheriting from sp as it was pointing to web
+web.using(AssignFrom(web2));
+// see below for more information on AssignFrom
+
+

Obserever Subscriptions

+

All timeline moments are exposed through the on property with three options for subscription.

+

Append

+

This is the default, and adds your observer to the end of the array of subscribed observers.

+
obj.on.log(function(this: Queryable, message: string, level: number) {
+    if (level > 1) {
+        console.log(message);
+    }
+});
+
+

Prepend

+

Using prepend will place your observer as the first item in the array of subscribed observers. There is no gaurantee it will always remain first, other code can also use prepend.

+
obj.on.log.prepend(function(this: Queryable, message: string, level: number) {
+    if (level > 1) {
+        console.log(message);
+    }
+});
+
+

Replace

+

Replace will remove all other subscribed observers from a moment and add the supplied observer as the only one in the array of subscribed observers.

+
obj.on.log.replace(function(this: Queryable, message: string, level: number) {
+    if (level > 1) {
+        console.log(message);
+    }
+});
+
+

ToArray

+

The ToArray method creates a cloned copy of the array of registered observers for a given moment. Note that because it is a clone changes to the returned array do not affect the registered observers.

+
const arr = obj.on.log.toArray();
+
+

Clear

+

This clears ALL observers for a given moment, returning true if any observers were removed, and false if no changes were made.

+
const didChange = obj.on.log.clear();
+
+

Special Behaviors

+

The core library includes two special behaviors used to help manage observer inheritance. The best case is to manage inheritance using the methods described above, but these provide quick shorthand to help in certain scenarios. These are AssignFrom and CopyFrom.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/storage/index.html b/core/storage/index.html new file mode 100644 index 000000000..b7a336f06 --- /dev/null +++ b/core/storage/index.html @@ -0,0 +1,2775 @@ + + + + + + + + + + + + + + + + + + + + + + + + storage - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/core : storage

+

This module provides a thin wrapper over the browser local and session storage. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.

+

PnPClientStorage

+

The main export of this module, contains properties representing local and session storage.

+
import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+const myvalue = storage.local.get("mykey");
+
+

PnPClientStorageWrapper

+

Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used +from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.

+
import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+
+// get a value from storage
+const value = storage.local.get("mykey");
+
+// put a value into storage
+storage.local.put("mykey2", "my value");
+
+// put a value into storage with an expiration
+storage.local.put("mykey2", "my value", new Date());
+
+// put a simple object into storage
+// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects
+storage.local.put("mykey3", {
+    key: "value",
+    key2: "value2",
+});
+
+// remove a value from storage
+storage.local.delete("mykey3");
+
+// get an item or add it if it does not exist
+// returns a promise in case you need time to get the value for storage
+// optionally takes a third parameter specifying the expiration
+storage.local.getOrPut("mykey4", () => {
+    return Promise.resolve("value");
+});
+
+// delete expired items
+storage.local.deleteExpired();
+
+

Cache Expiration

+

The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished by explicitly calling the deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.

+
import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+
+// session storage
+storage.session.deleteExpired();
+
+// local storage
+storage.local.deleteExpired();
+
+// this returns a promise, so you can perform some activity after the expired items are removed:
+storage.local.deleteExpired().then(_ => {
+    // init my application
+});
+
+

In previous versions we included code to automatically remove expired items. Due to a lack of necessity we removed that, but you can recreate the concept as shown below:

+
function expirer(timeout = 3000) {
+
+    // session storage
+    storage.session.deleteExpired();
+
+    // local storage
+    storage.local.deleteExpired();
+
+    setTimeout(() => expirer(timeout), timeout);
+}
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/timeline/index.html b/core/timeline/index.html new file mode 100644 index 000000000..e4eb75fbe --- /dev/null +++ b/core/timeline/index.html @@ -0,0 +1,2931 @@ + + + + + + + + + + + + + + + + + + + + + + + + Understanding Library Internals - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/core : timeline

+

Timeline provides base functionality for ochestrating async operations. A timeline defines a set of moments to which observers can be registered. Observers are functions that can act independently or together during a moment in the timeline. The model is event-like but each moment's implementation can be unique in how it interacts with the registered observers. Keep reading under Define Moments to understand more about what a moment is and how to create one.

+

Timeline Architecture

+

The easiest way to understand Timeline is to walk through implementing a simple one below. You also review Queryable to see how we use Timeline internally to the library.

+

Create a Timeline

+

Implementing a timeline involves several steps, each explained below.

+
    +
  1. Define Moments
  2. +
  3. Implement concrete Timeline class
  4. +
+

Define Moments

+

A timeline is made up of a set of moments which are themselves defined by a plain object with one or more properties, each of which is a function. You can use predefined moments, or create your own to meet your exact requirements. Below we define two moments within the MyMoments object, first and second. These names are entirely your choice and the order moments are defined in the plain object carries no meaning.

+

The first moment uses a pre-defined moment implementation asyncReduce. This moment allows you to define a state based on the arguments of the observer function, in this case FirstObserver. asyncReduce takes those arguments, does some processing, and returns a promise resolving an array matching the input arguments in order and type with optionally changed values. Those values become the arguments to the next observer registered to that moment.

+
import { asyncReduce, ObserverAction, Timeline } from "@pnp/core";
+
+// the first observer is a function taking a number and async returning a number in an array
+// all asyncReduce observers must follow this pattern of returning async a tuple matching the args
+export type FirstObserver = (this: any, counter: number) => Promise<[number]>;
+
+// the second observer is a function taking a number and returning void
+export type SecondObserver = (this: any, result: number) => void;
+
+// this is a custom moment definition as an example.
+export function report<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {
+
+    return function (observers: T[], ...args: any[]): void {
+
+        const obs = [...observers];
+
+        // for this 
+        if (obs.length > 0) {
+             Reflect.apply(obs[0], this, args);
+        }
+    };
+}
+
+// this plain object defines the moments which will be available in our timeline
+// the property name "first" and "second" will be the moment names, used when we make calls such as instance.on.first and instance.on.second
+const TestingMoments = {
+    first: asyncReduce<FirstObserver>(),
+    second: report<SecondObserver>(),
+} as const;
+// note as well the use of as const, this allows TypeScript to properly resolve all the complex typings and not treat the plain object as "any"
+
+

Subclass Timeline

+

After defining our moments we need to subclass Timeline to define how those moments emit through the lifecycle of the Timeline. Timeline has a single abstract method "execute" you must implement. You will also need to provide a way for callers to trigger the protected "start" method.

+
// our implementation of timeline, note we use `typeof TestingMoments` and ALSO pass the testing moments object to super() in the constructor
+class TestTimeline extends Timeline<typeof TestingMoments> {
+
+    // we create two unique refs for our implementation we will use
+    // to resolve the execute promise
+    private InternalResolveEvent = Symbol.for("Resolve");
+    private InternalRejectEvent = Symbol.for("Reject");
+
+    constructor() {
+        // we need to pass the moments to the base Timeline
+        super(TestingMoments);
+    }
+
+    // we implement the execute the method to define when, in what order, and how our moments are called. This give you full control within the Timeline framework
+    // to determine your implementation's behavior
+    protected async execute(init?: any): Promise<any> {
+
+        // we can always emit log to any subscribers
+        this.log("Starting", 0);
+
+        // set our timeline to start in the next tick
+        setTimeout(async () => {
+
+            try {
+
+                // we emit our "first" event
+                let [value] = await this.emit.first(init);
+
+                // we emit our "second" event
+                [value] = await this.emit.second(value);
+
+                // we reolve the execute promise with the final value
+                this.emit[this.InternalResolveEvent](value);
+
+            } catch (e) {
+
+                // we emit our reject event
+                this.emit[this.InternalRejectEvent](e);
+                // we emit error to any subscribed observers
+                this.error(e);
+            }
+        }, 0);
+
+        // return a promise which we will resolve/reject during the timeline lifecycle
+        return new Promise((resolve, reject) => {
+            this.on[this.InternalResolveEvent].replace(resolve);
+            this.on[this.InternalRejectEvent].replace(reject);
+        });
+    }
+
+    // provide a method to trigger our timeline, this could be protected or called directly by the user, your choice
+    public go(startValue = 0): Promise<number> {
+
+        // here we take a starting number
+        return this.start(startValue);
+    }
+}
+
+

Using your Timeline

+
import { TestTimeline } from "./file.js";
+
+const tl = new TestTimeline();
+
+// register observer
+tl.on.first(async (n) => [++n]);
+
+// register observer
+tl.on.second(async (n) => [++n]);
+
+// h === 2
+const h = await tl.go(0);
+
+// h === 7
+const h2 = await tl.go(5);
+
+

Understanding the Timeline Lifecycle

+

Now that you implemented a simple timeline let's take a minute to understand the lifecycle of a timeline execution. There are four moments always defined for every timeline: init, dispose, log, and error. Of these init and dispose are used within the lifecycle, while log and error are used as you need.

+

Timeline Lifecycle

+
    +
  • .on.init (always)
  • +
  • your moments as defined in execute, in our example:
  • +
  • .on.first
  • +
  • .on.second
  • +
  • .on.dispose (always)
  • +
+

As well the moments log and error exist on every Timeline derived class and can occur at any point during the lifecycle.

+

Observer Inheritance

+

Let's say that you want to contruct a system whereby you can create Timeline based instances from other Timeline based instances - which is what Queryable does. Imagine we have a class with a pseudo-signature like:

+
class ExampleTimeline extends Timeline<typeof SomeMoments> {
+
+    // we create two unique refs for our implementation we will use
+    // to resolve the execute promise
+    private InternalResolveEvent = Symbol.for("Resolve");
+    private InternalRejectEvent = Symbol.for("Reject");
+
+    constructor(base: ATimeline) {
+
+        // we need to pass the moments to the base Timeline
+        super(TestingMoments, base.observers);
+    }
+
+    //...
+}
+
+

We can then use it like:

+
const tl1 = new ExampleTimeline();
+tl1.on.first(async (n) => [++n]);
+tl1.on.second(async (n) => [++n]);
+
+// at this point tl2's observer collection is a pointer to the same collection as tl1
+const tl2 = new ExampleTimeline(tl1);
+
+// we add a second observer to first, it is applied to BOTH tl1 and tl2
+tl1.on.first(async (n) => [++n]);
+
+// BUT when we modify tl2's observers, either by adding or clearing a moment it begins to track its own collection
+tl2.on.first(async (n) => [++n]);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/core/util/index.html b/core/util/index.html new file mode 100644 index 000000000..7d088af47 --- /dev/null +++ b/core/util/index.html @@ -0,0 +1,2968 @@ + + + + + + + + + + + + + + + + + + + + + + + + util - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/core : util

+

This module contains utility methods that you can import individually from the core library.

+

combine

+

Combines any number of paths, normalizing the slashes as required

+
import { combine } from "@pnp/core";
+
+// "https://microsoft.com/something/more"
+const paths = combine("https://microsoft.com", "something", "more");
+
+// "also/works/with/relative"
+const paths2 = combine("/also/", "/works", "with/", "/relative\\");
+
+

dateAdd

+

Manipulates a date, please see the Stack Overflow discussion from which this method was taken.

+
import { dateAdd } from "@pnp/core";
+
+const now = new Date();
+
+const newData = dateAdd(now, "minute", 10);
+
+

getGUID

+

Creates a random guid, please see the Stack Overflow discussion from which this method was taken.

+
import { getGUID } from "@pnp/core";
+
+const newGUID = getGUID();
+
+

getRandomString

+

Gets a random string containing the number of characters specified.

+
import { getRandomString } from "@pnp/core";
+
+const randomString = getRandomString(10);
+
+

hOP

+

Shortcut for Object.hasOwnProperty. Determines if an object has a specified property.

+
import { HttpRequestError } from "@pnp/queryable";
+import { hOP } from "@pnp/core";
+
+export async function handleError(e: Error | HttpRequestError): Promise<void> {
+
+  //Checks to see if the error object has a property called isHttpRequestError. Returns a bool.
+  if (hOP(e, "isHttpRequestError")) {
+      // Handle this type or error
+  } else {
+    // not an HttpRequestError so we do something else
+
+  }
+}
+
+

jsS

+

Shorthand for JSON.stringify

+
import { jsS } from "@pnp/core";
+
+const s: string = jsS({ hello: "world" });
+
+

isArray

+

Determines if a supplied variable represents an array.

+
import { isArray } from "@pnp/core";
+
+const x = [1, 2, 3];
+
+if (isArray(x)){
+    console.log("I am an array");
+} else {
+    console.log("I am not an array");
+}
+
+

isFunc

+

Determines if a supplied variable represents a function.

+
import { isFunc } from "@pnp/core";
+
+public testFunction() {
+    console.log("test function");
+    return
+}
+
+if (isFunc(testFunction)){
+    console.log("this is a function");
+    testFunction();
+}
+
+

isUrlAbsolute

+

Determines if a supplied url is absolute, returning true; otherwise returns false.

+
import { isUrlAbsolute } from "@pnp/core";
+
+const webPath = 'https://{tenant}.sharepoint.com/sites/dev/';
+
+if (isUrlAbsolute(webPath)){
+    console.log("URL is absolute");
+}else{
+    console.log("URL is not absolute");
+}
+
+

objectDefinedNotNull

+

Determines if an object is defined and not null.

+
import { objectDefinedNotNull } from "@pnp/core";
+
+const obj = {
+    prop: 1
+};
+
+if (objectDefinedNotNull(obj)){
+    console.log("Not null");
+} else {
+    console.log("Null");
+}
+
+

stringIsNullOrEmpty

+

Determines if a supplied string is null or empty.

+
import { stringIsNullOrEmpty } from "@pnp/core";
+
+const x: string = "hello";
+
+if (stringIsNullOrEmpty(x)){
+    console.log("Null or empty");
+} else {
+    console.log("Not null or empty");
+}
+
+

getHashCode

+

Gets a (mostly) unique hashcode for a specified string.

+
+

Taken from: https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript

+
+
import { getHashCode } from "@pnp/core";
+
+const x: string = "hello";
+
+const hash = getHashCode(x);
+
+

delay

+

Provides an awaitable delay specified in milliseconds.

+
import { delay } from "@pnp/core";
+
+// wait 1 second
+await delay(1000);
+
+// wait 10 second
+await delay(10000);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/css/extra.css b/css/extra.css new file mode 100644 index 000000000..6391b1ff7 --- /dev/null +++ b/css/extra.css @@ -0,0 +1,33 @@ +.md-logo { + height: 32px; + width: 150px; + padding: 0 0.25 0.5 !important; +} + +.md-header{ + height: 75px; +} + +.md-container{ + padding-top: 70px; +} + +.md-sidebar[data-md-state="lock"]{ + padding-top: 75px; +} + +.md-logo img { + width: 100% !important; + height: auto !important; + margin-top: -0.25em; +} + +.md-footer { + margin-top: 5em; +} + +@media only screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__title--site .md-nav__button { + width: 150px; + } +} \ No newline at end of file diff --git a/debug-tests/index.html b/debug-tests/index.html new file mode 100644 index 000000000..91416d3fc --- /dev/null +++ b/debug-tests/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/debugging/index.html b/debugging/index.html new file mode 100644 index 000000000..360683468 --- /dev/null +++ b/debugging/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/documentation/index.html b/documentation/index.html new file mode 100644 index 000000000..613dce969 --- /dev/null +++ b/documentation/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/getting-started-dev/index.html b/getting-started-dev/index.html new file mode 100644 index 000000000..6ab9f96cc --- /dev/null +++ b/getting-started-dev/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/getting-started/index.html b/getting-started/index.html new file mode 100644 index 000000000..4bc99278a --- /dev/null +++ b/getting-started/index.html @@ -0,0 +1,3271 @@ + + + + + + + + + + + + + + + + + + + + + + + + Getting Started - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Getting Started

+

This library is geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.

+

If you need to support older browsers, SharePoint on-premisis servers, or older versions of the SharePoint Framework, please revert to version 2 of the library and see related documentation on polyfills for required functionality.

+

Minimal Requirements

+
- NodeJs: >= 14
+- TypeScript: 4.x
+- Node Modules Supported: ESM Only
+
+

Install

+

First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. @pnp/sp to access the SharePoint REST API and @pnp/graph to access some of the Microsoft Graph API. This step applies to any environment or project.

+

npm install @pnp/sp @pnp/graph --save

+

Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples.

+
import { getRandomString } from "@pnp/core";
+
+(function() {
+
+    // get and log a random string
+    console.log(getRandomString(20));
+
+})()
+
+

Getting Started with SharePoint Framework

+

The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises you will need to use version 2 of the library. If you are targeting SharePoint online you will need to take the additional steps outlined below based on the version of the SharePoint Framework you are targeting.

+

We've created two Getting Started samples. The first uses the more traditional React Component classes and can be found in the react-pnp-js-sample project, utilizing SPFx 1.15.2 and PnPjs V3, it showcases some of the more dramatic changes to the library. There is also a companion video series on YouTube if you prefer to see things done through that medium here's a link to the playlist for the 5 part series:

+

Getting started with PnPjs 3.0: 5-part series

+

In addition, we have converted the sample project from React Component to React Hooks. This version can be found in react-pnp-js-hooks. This sample will help those struggling to establish context correctly while using the hooks conventions.

+

The SharePoint Framework supports different versions of TypeScript natively and as of 1.14 release still doesn't natively support TypeScript 4.x. Sadly, this means that to use Version 3 of PnPjs you will need to take a few additional configuration steps to get them to work together.

+

SPFx Version 1.15.0 & later

+

No additional steps required

+

SPFx Version 1.12.1 => 1.14.0

+
    +
  1. +

    Update the rush stack compiler to 4.2. This is covered in this great article by Elio, but the steps are listed below.

    +
      +
    • Uninstall existing rush stack compiler (replace the ? with the version that is currently referenced in your package.json): + npm uninstall @microsoft/rush-stack-compiler-3.?
    • +
    • Install 4.2 version: + npm i @microsoft/rush-stack-compiler-4.2
    • +
    • Update tsconfig.json to extend the 4.2 config: + "extends": "./node_modules/@microsoft/rush-stack-compiler-4.2/includes/tsconfig-web.json"
    • +
    +
  2. +
  3. +

    Replace the contents of the gulpfile.js with: + >Note: The only change is the addition of the line to disable tslint.

    +

    ```js +'use strict';

    +

    const build = require('@microsoft/sp-build-web');

    +

    build.addSuppression(Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.);

    +

    var getTasks = build.rig.getTasks; +build.rig.getTasks = function () { + var result = getTasks.call(build.rig);

    +
    result.set('serve', result.get('serve-deprecated'));
    +
    +return result;
    +
    +

    };

    +

    // * ADDED * +// disable tslint +build.tslintCmd.enabled = false; +// * ADDED *

    +

    build.initialize(require('gulp')); +```

    +
  4. +
+

SPFx Version 1.11.0 & earlier

+

At this time there is no documented method to use version 3.x with SPFx versions earlier than 1.12.1. We recommend that you fall back to using version 2 of the library or update your SPFx version.

+

Imports and usage

+

Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. To establish context within the library you will need to use the SharePoint or Graph Factory Interface depending on which set of APIs you want to utilize. For SharePoint you will use the spfi interface and for the Microsoft Graph you will use the graphfi interface whic are both in the main export of the corresponding package. Examples of both methods are shown below.

+

Depending on how you architect your solution establishing context is done where you want to make calls to the API. The examples demonstrate doing so in the onInit method as a local variable but this could also be done to a private variable or passed into a service.

+
+

Note if you are going to use both the @pnp/sp and @pnp/graph packages in SPFx you will need to alias the SPFx behavior import, please see the section below for more details.

+
+

Using @pnp/sp spfi factory interface in SPFx

+
import { spfi, SPFx } from "@pnp/sp";
+
+// ...
+
+protected async onInit(): Promise<void> {
+
+    await super.onInit();
+    const sp = spfi().using(SPFx(this.context));
+
+}
+
+// ...
+
+
+

Using @pnp/graph graphfi factory interface in SPFx

+
import { graphfi, SPFx } from "@pnp/graph";
+
+// ...
+
+protected async onInit(): Promise<void> {
+
+    await super.onInit();
+    const graph = graphfi().using(SPFx(this.context));
+
+}
+
+// ...
+
+
+

Using both @pnp/sp and @pnp/graph in SPFx

+

+import { spfi, SPFx as spSPFx } from "@pnp/sp";
+import { graphfi, SPFx as graphSPFx} from "@pnp/graph";
+
+// ...
+
+protected async onInit(): Promise<void> {
+
+    await super.onInit();
+    const sp = spfi().using(spSPFx(this.context));
+    const graph = graphfi().using(graphSPFx(this.context));
+
+}
+
+// ...
+
+
+

Project Config/Services Setup

+

Please see the documentation on setting up a config file or a services for more information about establishing and instance of the spfi or graphfi interfaces that can be reused. It is a common mistake with users of V3 that they try and create the interface in event handlers which causes issues.

+

Getting started with NodeJS

+
+

Due to the way in which Node resolves ESM modules when you use selective imports in node you must include the index.js part of the path. Meaning an import like import "@pnp/sp/webs" in examples must be import "@pnp/sp/webs/index.js". Root level imports such as import { spfi } from "@pnp/sp" remain correct. The samples in this section demonstrate this for their selective imports.

+
+

Importing NodeJS support

+
+

Note that the NodeJS integration relies on code in the module @pnp/nodejs. It is therefore required that you import this near the beginning of your program, using simply

+

js +import "@pnp/nodejs";

+
+

Authentication

+

To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below.

+
mkdir \temp
+cd \temp
+openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'
+openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass
+
+
+

Using the above code you end up with three files, "cert.pem", "key.pem", and "keytmp.pem". The "cert.pem" file is uploaded to your AAD application registration. The "key.pem" is read as the private key for the configuration.

+
+

Using @pnp/sp spfi factory interface in NodeJS

+
+

Version 3 of this library only supports ESModules. If you still require commonjs modules please check out version 2.

+
+

The first step is to install the packages that will be needed. You can read more about what each package does starting on the packages page.

+
npm i @pnp/sp @pnp/nodejs
+
+

Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:

+

+import { SPDefault } from "@pnp/nodejs";
+import "@pnp/sp/webs/index.js";
+import { readFileSync } from 'fs';
+import { Configuration } from "@azure/msal-node";
+
+function() {
+    // configure your node options (only once in your application)
+    const buffer = readFileSync("c:/temp/key.pem");
+
+    const config: Configuration = {
+        auth: {
+            authority: "https://login.microsoftonline.com/{tenant id or common}/",
+            clientId: "{application (client) i}",
+            clientCertificate: {
+              thumbprint: "{certificate thumbprint, displayed in AAD}",
+              privateKey: buffer.toString(),
+            },
+        },
+    };
+
+    const sp = spfi().using(SPDefault({
+        baseUrl: 'https://{my tenant}.sharepoint.com/sites/dev/',
+        msal: {
+            config: config,
+            scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]
+        }
+    }));
+
+    // make a call to SharePoint and log it in the console
+    const w = await sp.web.select("Title", "Description")();
+    console.log(JSON.stringify(w, null, 4));
+}();
+
+

Using @pnp/graph graphfi factory interface in NodeJS

+

Similar to the above you can also make calls to the Microsoft Graph API from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example.

+
npm i @pnp/graph @pnp/nodejs
+
+

Now we need to import what we'll need to call graph

+
import { graphfi } from "@pnp/graph";
+import { GraphDefault } from "@pnp/nodejs";
+import "@pnp/graph/users/index.js";
+
+function() {
+    const graph = graphfi().using(GraphDefault({
+    baseUrl: 'https://graph.microsoft.com',
+    msal: {
+        config: config,
+        scopes: [ 'https://graph.microsoft.com/.default' ]
+    }
+    }));
+    // make a call to Graph and get all the groups
+    const userInfo = await graph.users.top(1)();
+    console.log(JSON.stringify(userInfo, null, 4));
+}();
+
+

Node project using TypeScript producing commonjs modules

+

For TypeScript projects which output commonjs but need to import esm modules you will need to take a few additional steps to use the pnp esm modules. This is true of any esm module with a project structured in this way, not specific to PnP's implementation. It is very possible there are other configurations that make this work, but these steps worked in our testing. We have also provided a basic sample showing this setup.

+

You must install TypeScript @next or you will get errors using node12 module resolution. This may change but is the current behavior when we did our testing.

+

npm install -D typescript@next

+

The tsconfig file for your project should have the "module": "CommonJS" and "moduleResolution": "node12", settings in addition to whatever else you need.

+

tsconfig.json

+
{
+    "compilerOptions": {
+        "module": "CommonJS",
+        "moduleResolution": "node12"
+}
+
+

You must then import the esm dependencies using the async import pattern. This works as expected with our selective imports, and vscode will pick up the intellisense as expected.

+

index.ts

+
import { settings } from "./settings.js";
+
+// this is a simple example as async await is not supported with commonjs output
+// at the root.
+setTimeout(async () => {
+
+    const { spfi } = await import("@pnp/sp");
+    const { SPDefault } = await import("@pnp/nodejs");
+    await import("@pnp/sp/webs/index.js");
+
+    const sp = spfi().using(SPDefault({
+        baseUrl: settings.testing.sp.url,
+        msal: {
+            config: settings.testing.sp.msal.init,
+            scopes: settings.testing.sp.msal.scopes
+        }
+    }));
+
+    // make a call to SharePoint and log it in the console
+    const w = await sp.web.select("Title", "Description")();
+    console.log(JSON.stringify(w, null, 4));
+
+}, 0);
+
+

Finally, when launching node you need to include the `` flag with a setting of 'node'.

+

node --experimental-specifier-resolution=node dist/index.js

+
+

Read more in the releated TypeScript Issue, TS pull request Adding the functionality, and the TS Docs.

+
+

Single Page Application Context

+

In some cases you may be working in a client-side application that doesn't have context to the SharePoint site. In that case you will need to utilize the MSAL Client, you can get the details on creating that connection in this article.

+

Selective Imports

+

This library has a lot of functionality and you may not need all of it. For that reason, we support selective imports which allow you to only import the parts of the sp or graph library you need, which reduces your overall solution bundle size - and enables treeshaking.

+

You can read more about selective imports.

+

Error Handling

+

This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.

+

Extending the Library

+

Because of the way the fluent library is designed by definition it's extendible. That means that if you want to build your own custom functions that extend the features of the library this can be done fairly simply. To get more information about creating your own custom extensions check out extending the library article.

+

Connect to a different Web

+

The new factory function allows you to create a connection to a different web maintaining the same setup as your existing interface. You have two options, either to 'AssignFrom' or 'CopyFrom' the base timeline's observers. The below example utilizes 'AssignFrom' but the method would be the same regadless of which route you choose. For more information on these behaviors see Core/Behaviors.

+
import { spfi, SPFx } from "@pnp/sp";
+import { AssignFrom } from "@pnp/core";
+import "@pnp/sp/webs";
+
+//Connection to the current context's Web
+const sp = spfi(...);
+
+// Option 1: Create a new instance of Queryable
+const spWebB = spfi({Other Web URL}).using(SPFx(this.context));
+
+// Option 2: Copy/Assign a new instance of Queryable using the existing
+const spWebB = spfi({Other Web URL}).using(AssignFrom(sp.web));
+
+// Option 3: Create a new instance of Queryable using other credentials?
+const spWebB = spfi({Other Web URL}).using(SPFx(this.context));
+
+// Option 4: Create new Web instance by using copying SPQuerable and new pointing to new web url (e.g. https://contoso.sharepoint.com/sites/Web2)
+const web = Web([sp.web, {Other Web URL}]);
+
+

Next Steps

+

For more complicated authentication scnearios please review the article describing all of the available authentication methods.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/behaviors/index.html b/graph/behaviors/index.html new file mode 100644 index 000000000..d0905baa2 --- /dev/null +++ b/graph/behaviors/index.html @@ -0,0 +1,2978 @@ + + + + + + + + + + + + + + + + + + + + + + + + behaviors - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph : behaviors

+

The article describes the behaviors exported by the @pnp/graph library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/nodejs.

+

DefaultInit

+

The DefaultInit behavior, itself a composed behavior includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit and ensures the request url is absolute.

+
import { graphfi, DefaultInit } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultInit());
+
+await graph.users();
+
+

DefaultHeaders

+

The DefaultHeaders behavior uses InjectHeaders to set the Content-Type header.

+
import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultHeaders());
+
+await graph.users();
+
+
+

DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.

+
+

Paged

+

Added in 3.4.0

+

The Paged behavior allows you to access the information in a collection through a series of pages. While you can use it directly, you will likely use the paged method of the collections which handles things for you.

+
+

Note that not all entity types support count and where it is unsupported it will return 0.

+
+

Basic example, read all users:

+
import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultHeaders());
+
+const allUsers = [];
+let users = await graph.users.top(300).paged();
+
+allUsers.push(...users.value);
+
+while (users.hasNext) {
+  users = await users.next();
+  allUsers.push(...users.value);
+}
+
+console.log(`All users: ${JSON.stringify(allUsers)}`);
+
+

Beyond the basics other query operations are supported such as filter and select.

+
import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(DefaultHeaders());
+
+const allUsers = [];
+let users = await graph.users.top(50).select("userPrincipalName", "displayName").filter("startswith(displayName, 'A')").paged();
+
+allUsers.push(...users.value);
+
+while (users.hasNext) {
+  users = await users.next();
+  allUsers.push(...users.value);
+}
+
+console.log(`All users: ${JSON.stringify(allUsers)}`);
+
+

And similarly for groups, showing the same pattern for different types of collections

+
import { graphfi, DefaultHeaders } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi().using(DefaultHeaders());
+
+const allGroups = [];
+let groups = await graph.groups.paged();
+
+allGroups.push(...groups.value);
+
+while (groups.hasNext) {
+  groups = await groups.next();
+  allGroups.push(...groups.value);
+}
+
+console.log(`All groups: ${JSON.stringify(allGroups)}`);
+
+

Endpoint

+

This behavior is used to change the endpoint to which requests are made, either "beta" or "v1.0". This allows you to easily switch back and forth between the endpoints as needed.

+
import { graphfi, Endpoint } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const beta = graphfi().using(Endpoint("beta"));
+
+const vOne = graphfi().using(Endpoint("v1.0"));
+
+await beta.users();
+
+await vOne.users();
+
+

It can also be used at any point in the fluid chain to switch an isolated request to a different endpoint.

+
import { graphfi, Endpoint } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// will point to v1 by default
+const graph = graphfi().using();
+
+const user = graph.users.getById("{id}");
+
+// this only applies to the "user" instance now
+const userInfoFromBeta = user.using(Endpoint("beta"))();
+
+

Finally, if you always want to make your requests to the beta end point (as an example) it is more efficient to set it in the graphfi factory.

+
import { graphfi } from "@pnp/graph";
+
+const beta = graphfi("https://graph.microsoft.com/beta");
+
+

GraphBrowser

+

A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.

+

The baseUrl prop can be used to configure the graph endpoint to which requests will be sent.

+
+

If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.

+
+
import { graphfi, GraphBrowser } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphBrowser());
+
+await graph.users();
+
+

You can also set a baseUrl. This is equivelent to calling graphfi with an absolute url.

+
import { graphfi, GraphBrowser } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(GraphBrowser({ baseUrl: "https://graph.microsoft.com/v1.0" }));
+
+// this is the same as the above, and maybe a litter easier to read, and is more efficient
+// const graph = graphfi("https://graph.microsoft.com/v1.0").using(GraphBrowser());
+
+await graph.users();
+
+

SPFx

+

This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. It also replaces any authentication present with a method to get a token from the SPFx aadTokenProviderFactory.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const graph = graphfi(...).using(SPFx(this.context));
+
+await graph.users();
+
+

Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.

+
import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'
+import { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'
+
+const sp = spfi().using(spSPFx(this.context));
+const graph = graphfi().using(graphSPFx(this.context));
+
+

If you want to use a different form of authentication you can apply that behavior after SPFx to override it. In this case we are using the client MSAL authentication.

+

SPFxToken

+

Added in 3.12

+

Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const graph = graphfi(...).using(SPFxToken(this.context));
+
+await graph.users();
+
+
import { graphfi } from "@pnp/graph";
+import { MSAL } from "@pnp/msaljsclient";
+import "@pnp/graph/users";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const graph = graphfi().using(SPFx(this.context), MSAL({ /* proper MSAL settings */}));
+
+await graph.users();
+
+

Telemetry

+

This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.

+
+

You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.

+
+
import { graphfi, Telemetry } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(Telemetry());
+
+await graph.users();
+
+

ConsistencyLevel

+

Using this behavior you can set the consistency level of your requests. You likely won't need to use this directly as we include it where needed.

+

Basic usage:

+
import { graphfi, ConsistencyLevel } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(ConsistencyLevel());
+
+await graph.users();
+
+

If in the future there is another value other than "eventual" you can supply it to the behavior. For now only "eventual" is a valid value, which is the default, so you do not need to pass it as a param.

+
import { graphfi, ConsistencyLevel } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi().using(ConsistencyLevel("{level value}"));
+
+await graph.users();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/bookings/index.html b/graph/bookings/index.html new file mode 100644 index 000000000..2fece735c --- /dev/null +++ b/graph/bookings/index.html @@ -0,0 +1,2907 @@ + + + + + + + + + + + + + + + + + + + + + + + + bookings - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/bookings

+

Represents the Bookings services available to a user.

+

You can learn more by reading the Official Microsoft Graph Documentation.

+

IBookingCurrencies, IBookingCurrency, IBookingBusinesses, IBookingBusiness, IBookingAppointments, IBookingAppointment, IBookingCustomers, IBookingCustomer, IBookingServices, IBookingService, IBookingStaffMembers, IBookingStaffMember, IBookingCustomQuestions, IBookingCustomQuestion

+

Invokable Banner Selective Imports Banner

+

Get Booking Currencies

+

Get the supported currencies

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+
+const graph = graphfi(...);
+
+// Get all the currencies
+const currencies = await graph.bookingCurrencies();
+// get the details of the first currency
+const currency = await graph.bookingCurrencies.getById(currencies[0].id)();
+
+

Work with Booking Businesses

+

Get the bookings businesses

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+
+const graph = graphfi(...);
+
+// Get all the businesses
+const businesses = await graph.bookingBusinesses();
+// get the details of the first business
+const business = graph.bookingBusinesses.getById(businesses[0].id)();
+const businessDetails = await business();
+// get the business calendar
+const calView = await business.calendarView("2022-06-01", "2022-08-01")();
+// publish the business
+await business.publish();
+// unpublish the business
+await business.unpublish();
+
+

Work with Booking Services

+

Get the bookings business services

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingService } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business services
+const services = await business.services();
+// add a service
+const newServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};
+const newService = services.add(newServiceDesc);
+// get service by id
+const service = await business.services.getById({service id})();
+// update service
+const updateServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};
+const update = await business.services.getById({service id}).update(updateServiceDesc);
+// delete service
+await business.services.getById({service id}).delete();
+
+

Work with Booking Customers

+

Get the bookings business customers

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingCustomer } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business customers
+const customers = await business.customers();
+// add a customer
+const newCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};
+const newCustomer = customers.add(newCustomerDesc);
+// get customer by id
+const customer = await business.customers.getById({customer id})();
+// update customer
+const updateCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};
+const update = await business.customers.getById({customer id}).update(updateCustomerDesc);
+// delete customer
+await business.customers.getById({customer id}).delete();
+
+

Work with Booking StaffMembers

+

Get the bookings business staffmembers

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingStaffMember } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business staff members
+const staffmembers = await business.staffMembers();
+// add a staff member
+const newStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};
+const newStaffMember = staffmembers.add(newStaffMemberDesc);
+// get staff member by id
+const staffmember = await business.staffMembers.getById({staff member id})();
+// update staff member
+const updateStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};
+const update = await business.staffMembers.getById({staff member id}).update(updateStaffMemberDesc);
+// delete staffmember
+await business.staffMembers.getById({staff member id}).delete();
+
+

Work with Booking Appointments

+

Get the bookings business appointments

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingAppointment } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business appointments
+const appointments = await business.appointments();
+// add a appointment
+const newAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};
+const newAppointment = appointments.add(newAppointmentDesc);
+// get appointment by id
+const appointment = await business.appointments.getById({appointment id})();
+// cancel the appointment
+await appointment.cancel();
+// update appointment
+const updateAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};
+const update = await business.appointments.getById({appointment id}).update(updateAppointmentDesc);
+// delete appointment
+await business.appointments.getById({appointment id}).delete();
+
+

Work with Booking Custom Questions

+

Get the bookings business custom questions

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/bookings";
+import { BookingCustomQuestion } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const business = graph.bookingBusinesses.getById({Booking Business Id})();
+// get the business custom questions
+const customQuestions = await business.customQuestions();
+// add a custom question
+const newCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};
+const newCustomQuestion = customQuestions.add(newCustomQuestionDesc);
+// get custom question by id
+const customquestion = await business.customQuestions.getById({customquestion id})();
+// update custom question
+const updateCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};
+const update = await business.customQuestions.getById({custom question id}).update(updateCustomQuestionDesc);
+// delete custom question
+await business.customQuestions.getById({customquestion id}).delete();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/calendars/index.html b/graph/calendars/index.html new file mode 100644 index 000000000..c28fc54a9 --- /dev/null +++ b/graph/calendars/index.html @@ -0,0 +1,3063 @@ + + + + + + + + + + + + + + + + + + + + + + + + calendars - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/calendars

+

More information can be found in the official Graph documentation:

+ +

ICalendar, ICalendars

+

Invokable Banner Selective Imports Banner

+

Get All Calendars For a User

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars();
+
+const myCalendars = await graph.me.calendars();
+
+
+

Get a Specific Calendar For a User

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';
+
+const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)();
+
+const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();
+
+

Get a User's Default Calendar

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar();
+
+const myCalendar = await graph.me.calendar();
+
+

Get Events For a User's Default Calendar

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+// You can get the default calendar events
+const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events();
+// or get all events for the user
+const events = await graph.users.getById('user@tenant.onmicrosoft.com').events();
+
+// You can get my default calendar events
+const events = await graph.me.calendar.events();
+// or get all events for me
+const events = await graph.me.events();
+
+

Get Events By ID

+

You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar.

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA==';
+
+const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';
+
+// Get events by ID
+const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID);
+
+const events = await graph.me.events.getByID(EventID);
+
+// Get an event by ID from a specific calendar
+const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID);
+
+const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);
+
+
+

Create Events

+

This will work on any IEvents objects (e.g. anything accessed using an events key).

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add(
+{
+  "subject": "Let's go for lunch",
+  "body": {
+    "contentType": "HTML",
+    "content": "Does late morning work for you?"
+  },
+  "start": {
+      "dateTime": "2017-04-15T12:00:00",
+      "timeZone": "Pacific Standard Time"
+  },
+  "end": {
+      "dateTime": "2017-04-15T14:00:00",
+      "timeZone": "Pacific Standard Time"
+  },
+  "location":{
+      "displayName":"Harry's Bar"
+  },
+  "attendees": [
+    {
+      "emailAddress": {
+        "address":"samanthab@contoso.onmicrosoft.com",
+        "name": "Samantha Booth"
+      },
+      "type": "required"
+    }
+  ]
+});
+
+

Update Events

+

This will work on any IEvents objects (e.g. anything accessed using an events key).

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';
+
+await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({
+    reminderMinutesBeforeStart: 99,
+});
+
+

Delete Event

+

This will work on any IEvents objects (e.g. anything accessed using an events key).

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';
+
+await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete();
+
+await graph.me.events.getById(EVENT_ID).delete();
+
+

Get Calendar for a Group

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/groups';
+
+const graph = graph.using(SPFx(this.context));
+
+const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();
+
+

Get Events for a Group

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/groups';
+
+const graph = graphfi(...);
+
+// You can do one of
+const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events();
+// or
+const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();
+
+

Get Calendar View

+

Gets the events in a calendar during a specified date range.

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+
+// basic request, note need to invoke the returned queryable
+const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01", "2020-03-01")();
+
+// you can use select, top, etc to filter your returned results
+const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01", "2020-03-01").select("subject").top(3)();
+
+// you can specify times along with the dates
+const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01T19:00:00-08:00", "2020-03-01T19:00:00-08:00")();
+
+const view4 = await graph.me.calendarView("2020-01-01", "2020-03-01")();
+
+

Find Rooms

+

Gets the emailAddress objects that represent all the meeting rooms in the user's tenant or in a specific room list.

+

Beta Endpoint

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+// basic request, note need to invoke the returned queryable
+const rooms1 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms()();
+// you can pass a room list to filter results
+const rooms2 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms('roomlist@tenant.onmicrosoft.com')();
+// you can use select, top, etc to filter your returned results
+const rooms3 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms().select('name').top(10)();
+
+

Get Event Instances

+

Get the instances (occurrences) of an event for a specified time range.

+

If the event is a seriesMaster type, this returns the occurrences and exceptions of the event in the specified time range.

+
import { graphfi } from "@pnp/graph";
+import '@pnp/graph/calendars';
+import '@pnp/graph/users';
+
+const graph = graphfi(...);
+const event = graph.me.events.getById('');
+// basic request, note need to invoke the returned queryable
+const instances = await event.instances("2020-01-01", "2020-03-01")();
+// you can use select, top, etc to filter your returned results
+const instances2 = await event.instances("2020-01-01", "2020-03-01").select("subject").top(3)();
+// you can specify times along with the dates
+const instance3 = await event.instances("2020-01-01T19:00:00-08:00", "2020-03-01T19:00:00-08:00")(); 
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/cloud-communications/index.html b/graph/cloud-communications/index.html new file mode 100644 index 000000000..1bbbfe56b --- /dev/null +++ b/graph/cloud-communications/index.html @@ -0,0 +1,2719 @@ + + + + + + + + + + + + + + + + + + + + + + + + cloud communications - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/cloud-communications

+

The ability to retrieve information about a user's presence, including their availability and user activity.

+

More information can be found in the official Graph documentation:

+ +

IPresence

+

Invokable Banner Selective Imports Banner

+

Get users presence

+

Gets a list of all the contacts for the user.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/cloud-communications";
+
+const graph = graphfi(...);
+
+const presenceMe = await graph.me.presence();
+
+const presenceThem = await graph.users.getById("99999999-9999-9999-9999-999999999999").presence();
+
+
+

Get presence for multiple users

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/cloud-communications";
+
+const graph = graphfi(...);
+
+const presenceList = await graph.communications.getPresencesByUserId(["99999999-9999-9999-9999-999999999999"]);
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/columns/index.html b/graph/columns/index.html new file mode 100644 index 000000000..e1912d29a --- /dev/null +++ b/graph/columns/index.html @@ -0,0 +1,2837 @@ + + + + + + + + + + + + + + + + + + + + + + + + columns - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Graph Columns

+

More information can be found in the official Graph documentation:

+ +

Selective Imports Banner

+

Get Columns

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+//Needed for content types
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const siteColumns = await graph.site.getById("{site identifier}").columns();
+const listColumns = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").columns();
+const contentTypeColumns = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").columns();
+
+

Get Columns by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+//Needed for content types
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const siteColumn = await graph.site.getById("{site identifier}").columns.getById("{column identifier}")();
+const listColumn = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").columns.getById("{column identifier}")();
+const contentTypeColumn = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").columns.getById("{column identifier}")();
+
+

Add a Columns (Sites and List)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const sampleColumn: ColumnDefinition = {
+    description: "PnPTestColumn Description",
+    enforceUniqueValues: false,
+    hidden: false,
+    indexed: false,
+    name: "PnPTestColumn",
+    displayName: "PnPTestColumn",
+    text: {
+        allowMultipleLines: false,
+        appendChangesToExistingText: false,
+        linesForEditing: 0,
+        maxLength: 255,
+    },
+};
+
+const siteColumn = await graph.site.getById("{site identifier}").columns.add(sampleColumn);
+const listColumn = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").columns.add(sampleColumn);
+
+

Add a Column Reference (Content Types)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for content types
+import "@pnp/graph/content-ypes";
+
+const graph = graphfi(...);
+
+const siteColumn = await graph.site.getById("{site identifier}").columns.getById("{column identifier}")();
+const contentTypeColumn = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").columns.addRef(siteColumn);
+
+

Update a Column (Sites and List)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const site = graph.site.getById("{site identifier}");
+const updatedSiteColumn = await site.columns.getById("{column identifier}").update({ displayName: "New Name" });
+const updateListColumn = await site.lists.getById("{list identifier}").columns.getById("{column identifier}").update({ displayName: "New Name" });
+
+

Delete a Column

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+//Needed for content types
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const site = graph.site.getById("{site identifier}");
+const siteColumn = await site.columns.getById("{column identifier}").delete();
+const listColumn = await site.lists.getById("{list identifier}").columns.getById("{column identifier}").delete();
+const contentTypeColumn = await site.contentTypes.getById("{content type identifier}").columns.getById("{column identifier}").delete();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/contacts/index.html b/graph/contacts/index.html new file mode 100644 index 000000000..0beec13f3 --- /dev/null +++ b/graph/contacts/index.html @@ -0,0 +1,3118 @@ + + + + + + + + + + + + + + + + + + + + + + + + contacts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/contacts

+

The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graphfi(). Through the methods described +you can add and edit both contacts and folders in a users Outlook.

+

More information can be found in the official Graph documentation:

+ +

IContact, IContacts, IContactFolder, IContactFolders

+

Invokable Banner Selective Imports Banner

+

Set up notes

+

To make user calls you can use getById where the id is the users email address. +Contact ID, Folder ID, and Parent Folder ID use the following format "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA="

+

Get all of the Contacts

+

Gets a list of all the contacts for the user.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users"
+import "@pnp/graph/contacts"
+
+const graph = graphfi(...);
+
+const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts();
+
+const contacts2 = await graph.me.contacts();
+
+
+

Get Contact by Id

+

Gets a specific contact by ID for the user.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
+
+const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)();
+
+const contact2 = await graph.me.contacts.getById(contactID)();
+
+
+

Add a new Contact

+

Adds a new contact for the user.

+
import { graphfi } from "@pnp/graph";
+import { EmailAddress } from "@microsoft/microsoft-graph-types";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+
+

Update a Contact

+

Updates a specific contact by ID for teh designated user

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
+
+const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: "1986-05-30" });
+
+const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: "1986-05-30" });
+
+
+

Delete a Contact

+

Delete a contact from the list of contacts for a user.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
+
+const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete();
+
+const delContact2 = await graph.me.contacts.getById(contactID).delete();
+
+
+

Get all of the Contact Folders

+

Get all the folders for the designated user's contacts

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders();
+
+const contactFolders2 = await graph.me.contactFolders();
+
+
+

Get Contact Folder by Id

+

Get a contact folder by ID for the specified user

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)();
+
+const contactFolder2 = await graph.me.contactFolders.getById(folderID)();
+
+
+

Add a new Contact Folder

+

Add a new folder in the users contacts

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const parentFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=";
+
+const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add("New Folder", parentFolderID);
+
+const addedContactFolder2 = await graph.me.contactFolders.add("New Folder", parentFolderID);
+
+
+

Update a Contact Folder

+

Update an existing folder in the users contacts

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: "Updated Folder" });
+
+const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: "Updated Folder" });
+
+
+

Delete a Contact Folder

+

Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete();
+
+const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();
+
+
+

Get all of the Contacts from the Contact Folder

+

Get all the contacts in a folder

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts();
+
+const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();
+
+
+

Get Child Folders of the Contact Folder

+

Get child folders from contact folder

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders();
+
+const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();
+
+
+

Add a new Child Folder

+

Add a new child folder to a contact folder

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+
+const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add("Sub Folder", folderID);
+
+const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add("Sub Folder", folderID);
+
+

Get Child Folder by Id

+

Get child folder by ID from user contacts

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+const subFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=";
+
+const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)();
+
+const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();
+
+

Add Contact in Child Folder of Contact Folder

+

Add a new contact to a child folder

+
import { graphfi } from "@pnp/graph";
+import { EmailAddress } from "./@microsoft/microsoft-graph-types";
+import "@pnp/graph/users";
+import "@pnp/graph/contacts";
+
+const graph = graphfi(...);
+
+const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
+const subFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=";
+
+const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/content-types/index.html b/graph/content-types/index.html new file mode 100644 index 000000000..777ceda44 --- /dev/null +++ b/graph/content-types/index.html @@ -0,0 +1,2961 @@ + + + + + + + + + + + + + + + + + + + + + + + + content types - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Graph Content Types

+

More information can be found in the official Graph documentation:

+ +

Selective Imports Banner

+

Get Content Types

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteContentTypes = await graph.site.getById("{site identifier}").contentTypes();
+const listContentTypes = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes();
+
+

Get Content Types by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteContentType = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}")();
+const listContentType = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getById("{content type identifier}")();
+
+

Add a Content Type (Site)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const sampleContentType: ContentType = {
+    name: "PnPTestContentType",
+    description: "PnPTestContentType Description",
+    base: {
+        name: "Item",
+        id: "0x01",
+    },
+    group: "PnPTest Content Types",
+    id: "0x0100CDB27E23CEF44850904C80BD666FA645",
+};
+
+const siteContentType = await graph.sites.getById("{site identifier}").contentTypes.add(sampleContentType);
+
+

Add a Content Type - Copy (List)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/lists";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+//Get a list of compatible site content types for the list
+const siteContentType = await graph.site.getById("{site identifier}").getApplicableContentTypesForList("{list identifier}")();
+//Get a specific content type from the site.
+const siteContentType = await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}")();
+const listContentType = await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.addCopy(siteContentType);
+
+

Update a Content Type (Sites and List)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/columns";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const site = graph.site.getById("{site identifier}");
+const updatedSiteContentType = await site.contentTypes.getById("{content type identifier}").update({ description: "New Description" });
+const updateListContentType = await site.lists.getById("{list identifier}").contentTypes.getById("{content type identifier}").update({ description: "New Description" });
+
+

Delete a Content Type

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+await graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}").delete();
+await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getById("{content type identifier}").delete();
+
+

Get Compatible Content Types from Hub

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteContentTypes = await graph.site.getById("{site identifier}").contentTypes.getCompatibleHubContentTypes();
+const listContentTypes = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getCompatibleHubContentTypes();
+
+

Add/Sync Content Types from Hub (Site and List)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+//Needed for lists
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const hubSiteContentTypes = await graph.site.getById("{site identifier}").contentTypes.getCompatibleHubContentTypes();
+const siteContentType = await graph.site.getById("{site identifier}").contentTypes.addCopyFromContentTypeHub(hubSiteContentTypes[0].Id);
+
+const hubListContentTypes = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.getCompatibleHubContentTypes();
+const listContentType = await graph.site.getById("{site identifier}").lists.getById("{list identifier}").contentTypes.addCopyFromContentTypeHub(hubListContentTypes[0].Id);
+
+

Site Content Type (isPublished, Publish, Unpublish)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const siteContentType = graph.site.getById("{site identifier}").contentTypes.getById("{content type identifier}");
+const isPublished = await siteContentType.isPublished();
+await siteContentType.publish();
+await siteContentType.unpublish();;
+
+

Associate Content Type with Hub Sites

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+const hubSiteUrls: string[] = [hubSiteUrl1, hubSiteUrl2, hubSiteUrl3];
+const propagateToExistingLists = true;
+// NOTE: the site must be the content type hub
+const contentTypeHub = graph.site.getById("{content type hub site identifier}");
+const siteContentType = await contentTypeHub.contentTypes.getById("{content type identifier}").associateWithHubSites(hubSiteUrls, propagateToExistingLists);
+
+

Copy a file to a default content location in a content type

+
+

Not fully implemented, requires Files support

+
+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+import "@pnp/graph/content-types";
+
+const graph = graphfi(...);
+
+// Not fully implemented
+const sourceFile: ItemReference = {};
+const destinationFileName: string = "NewFileName";
+
+const site = graph.site.getById("{site identifier}");
+const siteContentType = await site.contentTypes.getById("{content type identifier}").copyToDefaultContentLocation(sourceFile, destinationFileName);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/directoryobjects/index.html b/graph/directoryobjects/index.html new file mode 100644 index 000000000..69728fec7 --- /dev/null +++ b/graph/directoryobjects/index.html @@ -0,0 +1,2829 @@ + + + + + + + + + + + + + + + + + + + + + + + + directory objects - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/directoryObjects

+

Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types.

+

More information can be found in the official Graph documentation:

+ +

IDirectoryObject, IDirectoryObjects

+

Invokable Banner Selective Imports Banner

+

The groups and directory roles for the user

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf();
+
+const memberOf2 = await graph.me.memberOf();
+
+
+

Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups();
+
+const memberGroups2 = await graph.me.getMemberGroups();
+
+// Returns only security enabled groups
+const memberGroups3 = await graph.me.getMemberGroups(true);
+
+const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();
+
+
+

Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects();
+
+const memberObjects2 = await graph.me.getMemberObjects();
+
+// Returns only security enabled groups
+const memberObjects3 = await graph.me.getMemberObjects(true);
+
+const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();
+
+

Check for membership in a specified list of groups

+

And returns from that list those groups of which the specified user, group, or directory object is a member

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers2 = await graph.me.checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+

Get directoryObject by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/directory-objects";
+
+const graph = graphfi(...);
+
+const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');
+
+
+

Delete directoryObject

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/directory-objects";
+
+const graph = graphfi(...);
+
+const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/groups/index.html b/graph/groups/index.html new file mode 100644 index 000000000..704662c48 --- /dev/null +++ b/graph/groups/index.html @@ -0,0 +1,2943 @@ + + + + + + + + + + + + + + + + + + + + + + + + groups - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/groups

+

Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent.

+

Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups.

+

You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation.

+

IGroup, IGroups

+

Invokable Banner Selective Imports Banner

+

Add a Group

+

Add a new group.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import { GroupType } from '@pnp/graph/groups';
+
+const graph = graphfi(...);
+
+const groupAddResult = await graph.groups.add("GroupName", "Mail_NickName", GroupType.Office365);
+const group = await groupAddResult.group();
+
+

Delete a Group

+

Deletes an existing group.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").delete();
+
+

Update Group Properties

+

Updates an existing group.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").update({ displayName: newName, propertyName: updatedValue});
+
+

Add favorite

+

Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").addFavorite();
+
+

Remove favorite

+

Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").removeFavorite();
+
+

Reset Unseen Count

+

Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").resetUnseenCount();
+
+

Subscribe By Mail

+

Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").subscribeByMail();
+
+

Unsubscribe By Mail

+

Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").unsubscribeByMail();
+
+

Get Calendar View

+

Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const startDate = new Date("2020-04-01");
+const endDate = new Date("2020-03-01");
+
+const events = graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").getCalendarView(startDate, endDate);
+
+

Group Photo Operations

+

See Photos

+

Group Membership

+

Get the members and/or owners of a group.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/members";
+
+const graph = graphfi(...);
+const members = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").members();
+const owners = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").owners();
+
+

Get the Team Site for a Group

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/sites/group";
+
+const graph = graphfi(...);
+
+const teamSite = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").sites.root();
+const url = teamSite.webUrl
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/insights/index.html b/graph/insights/index.html new file mode 100644 index 000000000..d3594133b --- /dev/null +++ b/graph/insights/index.html @@ -0,0 +1,2911 @@ + + + + + + + + + + + + + + + + + + + + + + + + insights - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/insights

+

This module helps you get Insights in form of Trending, Used and Shared. The results are based on relationships calculated using advanced analytics and machine learning techniques.

+

IInsights

+

Invokable Banner Selective Imports Banner

+ +

Returns documents from OneDrive and SharePoint sites trending around a user.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const trending = await graph.me.insights.trending()
+
+const trending = await graph.users.getById("userId").insights.trending()
+
+ +

Using the getById method to get a trending document by Id.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const trendingDoc = await graph.me.insights.trending.getById('Id')()
+
+const trendingDoc = await graph.users.getById("userId").insights.trending.getById('Id')()
+
+ +

Using the resources method to get the resource from a trending document.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const resource = await graph.me.insights.trending.getById('Id').resource()
+
+const resource = await graph.users.getById("userId").insights.trending.getById('Id').resource()
+
+

Get all Used documents

+

Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const used = await graph.me.insights.used()
+
+const used = await graph.users.getById("userId").insights.used()
+
+

Get a Used document by Id

+

Using the getById method to get a used document by Id.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const usedDoc = await graph.me.insights.used.getById('Id')()
+
+const usedDoc = await graph.users.getById("userId").insights.used.getById('Id')()
+
+

Get the resource from Used document

+

Using the resources method to get the resource from a used document.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const resource = await graph.me.insights.used.getById('Id').resource()
+
+const resource = await graph.users.getById("userId").insights.used.getById('Id').resource()
+
+

Get all Shared documents

+

Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const shared = await graph.me.insights.shared()
+
+const shared = await graph.users.getById("userId").insights.shared()
+
+

Get a Shared document by Id

+

Using the getById method to get a shared document by Id.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const sharedDoc = await graph.me.insights.shared.getById('Id')()
+
+const sharedDoc = await graph.users.getById("userId").insights.shared.getById('Id')()
+
+

Get the resource from a Shared document

+

Using the resources method to get the resource from a shared document.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/insights";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const resource = await graph.me.insights.shared.getById('Id').resource()
+
+const resource = await graph.users.getById("userId").insights.shared.getById('Id').resource()
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/invitations/index.html b/graph/invitations/index.html new file mode 100644 index 000000000..35677a81c --- /dev/null +++ b/graph/invitations/index.html @@ -0,0 +1,2690 @@ + + + + + + + + + + + + + + + + + + + + + + + + invitations - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/invitations

+

The ability invite an external user via the invitation manager

+

IInvitations

+

Invokable Banner Selective Imports Banner

+

Create Invitation

+

Using the invitations.create() you can create an Invitation. +We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/invitations";
+
+const graph = graphfi(...);
+
+const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/items/index.html b/graph/items/index.html new file mode 100644 index 000000000..747c72778 --- /dev/null +++ b/graph/items/index.html @@ -0,0 +1,2741 @@ + + + + + + + + + + + + + + + + + + + + + + + + items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/items

+

Currently, there is no module in graph to access all items directly. Please, instead, default to search by path using the following methods.

+

Selective Imports Banner

+

Get list items

+
import { Site } from "@pnp/graph/sites";
+
+const sites = graph.sites.getById("{site id}");
+
+const items = await Site(sites, "lists/{listid}/items")();
+
+

Get File/Item version information

+
import { Site } from "@pnp/graph/sites";
+
+const sites = graph.sites.getById("{site id}");
+
+const users = await Site(sites, "lists/{listid}/items/{item id}/versions")();
+
+

Get list items with fields included

+
import { Site } from "@pnp/graph/sites";
+import "@pnp/graph/lists";
+
+const sites = graph.sites.getById("{site id}");
+
+const listItems : IList[] = await Site(sites, "lists/{site id}/items?$expand=fields")();
+
+ + + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/lists/index.html b/graph/lists/index.html new file mode 100644 index 000000000..7bd44a76c --- /dev/null +++ b/graph/lists/index.html @@ -0,0 +1,2809 @@ + + + + + + + + + + + + + + + + + + + + + + + + lists - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/lists

+

More information can be found in the official Graph documentation:

+ +

Selective Imports Banner

+

Get Lists

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const siteLists = await graph.site.getById("{site identifier}").lists();
+
+

Get List by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const listInfo = await graph.sites.getById("{site identifier}").lists.getById("{list identifier}")();
+
+

Add a List

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const sampleList: List = {
+    displayName: "PnPGraphTestList",
+    list: { "template": "genericList" },
+};
+
+const list = await graph.sites.getById("{site identifier}").lists.add(listTemplate);
+
+

Update a List

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const list = await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").update({ displayName: "MyNewListName" });
+
+

Delete a List

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").delete();
+
+

Get List Columns

+

For more information about working please see documentation on columns

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/lists";
+import "@pnp/graph/columns";
+
+const graph = graphfi(...);
+
+await graph.sites.getById("{site identifier}").lists.getById("{list identifier}").columns();
+
+

Get List Items

+

Currently, recieving list items via @pnpjs/graph API is not possible.

+

This can currently be done with a call by path as documented under @pnpjs/graph/items

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/messages/index.html b/graph/messages/index.html new file mode 100644 index 000000000..588624e92 --- /dev/null +++ b/graph/messages/index.html @@ -0,0 +1,2677 @@ + + + + + + + + + + + + + + + + + + + + + + + + messages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Graph Messages (Mail)

+

More information can be found in the official Graph documentation:

+ +

Selective Imports Banner

+

Get User's Messages

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/messages";
+
+const graph = graphfi(...);
+
+const currentUser = graph.me;
+const messages = await currentUser.messages();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/onedrive/index.html b/graph/onedrive/index.html new file mode 100644 index 000000000..ff30c63d8 --- /dev/null +++ b/graph/onedrive/index.html @@ -0,0 +1,3528 @@ + + + + + + + + + + + + + + + + + + + + + + + + onedrive - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/onedrive

+

The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can manage drives and drive items in Onedrive.

+

IInvitations

+

Invokable Banner Selective Imports Banner

+

Get the default drive

+

Using the drive you can get the users default drive from Onedrive, or the groups or sites default document library.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+import "@pnp/graph/sites";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const otherUserDrive = await graph.users.getById("user@tenant.onmicrosoft.com").drive();
+
+const currentUserDrive = await graph.me.drive();
+
+const groupDrive = await graph.groups.getById("{group identifier}").drive();
+
+const siteDrive = await graph.sites.getById("{site identifier}").drive();
+
+

Get all of the drives

+

Using the drives() you can get the users available drives from Onedrive

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/groups";
+import "@pnp/graph/sites";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const otherUserDrive = await graph.users.getById("user@tenant.onmicrosoft.com").drives();
+
+const currentUserDrive = await graph.me.drives();
+
+const groupDrives = await graph.groups.getById("{group identifier}").drives();
+
+const siteDrives = await graph.sites.getById("{site identifier}").drives();
+
+
+

Get drive by Id

+

Using the drives.getById() you can get one of the available drives in Outlook

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const drive = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}")();
+
+const drive = await graph.me.drives.getById("{drive id}")();
+
+const drive = await graph.drives.getById("{drive id}")();
+
+
+

Get the associated list of a drive

+

Using the list() you get the associated list information

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const list = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").list();
+
+const list = await graph.me.drives.getById("{drive id}").list();
+
+
+

Using the getList(), from the lists implementation, you get the associated IList object. +Form more infomration about acting on the IList object see @pnpjs/graph/lists

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/lists";
+
+const graph = graphfi(...);
+
+const listObject: IList = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").getList();
+
+const listOBject: IList = await graph.me.drives.getById("{drive id}").getList();
+
+const list = await listObject();
+
+

Get the recent files

+

Using the recent() you get the recent files

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const files = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").recent();
+
+const files = await graph.me.drives.getById("{drive id}").recent();
+
+
+

Get the files shared with me

+

Using the sharedWithMe() you get the files shared with the user

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const shared = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").sharedWithMe();
+
+const shared = await graph.me.drives.getById("{drive id}").sharedWithMe();
+
+// By default, sharedWithMe return items shared within your own tenant. To include items shared from external tenants include the options object.
+
+const options: ISharingWithMeOptions = {allowExternal: true};
+const shared = await graph.me.drives.getById("{drive id}").sharedWithMe(options);
+
+
+

Get the following files

+

List the items that have been followed by the signed in user.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const files = await graph.me.drives.getById("{drive id}").following();
+
+
+

Get the Root folder

+

Using the root() you get the root folder

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/sites";
+import "@pnp/graph/groups";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const root = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root();
+const root = await graph.users.getById("user@tenant.onmicrosoft.com").drive.root();
+
+const root = await graph.me.drives.getById("{drive id}").root();
+const root = await graph.me.drive.root();
+
+const root = await graph.sites.getById("{site id}").drives.getById("{drive id}").root();
+const root = await graph.sites.getById("{site id}").drive.root();
+
+const root = await graph.groups.getById("{site id}").drives.getById("{drive id}").root();
+const root = await graph.groups.getById("{site id}").drive.root();
+
+
+

Get the Children

+

Using the children() you get the children

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const rootChildren = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.children();
+
+const rootChildren = await graph.me.drives.getById("{drive id}").root.children();
+
+const itemChildren = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").children();
+
+const itemChildren = await graph.me.drives.getById("{drive id}").root.items.getById("{item id}").children();
+
+
+

Get the children by path

+

Using the drive.getItemsByPath() you can get the contents of a particular folder path

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const item = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getItemsByPath("MyFolder/MySubFolder")();
+
+const item = await graph.me.drives.getItemsByPath("MyFolder/MySubFolder")();
+
+
+

Add Item

+

Using the add you can add an item, for more options please user the upload method instead.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/users";
+import {IDriveItemAddResult} from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const add1: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.children.add("test.txt", "My File Content String");
+const add2: IDriveItemAddResult = await graph.me.drives.getById("{drive id}").root.children.add("filename.txt", "My File Content String");
+
+

Upload/Replace Drive Item Content

+

Using the .upload method you can add or update the content of an item.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/users";
+import {IFileOptions, IDriveItemAddResult} from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// file path is only file name
+const fileOptions: IFileOptions = {
+    content: "This is some test content",
+    filePathName: "pnpTest.txt",
+    contentType: "text/plain;charset=utf-8"
+}
+
+const uDriveRoot: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drive.root.upload(fileOptions);
+
+const uFolder: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drive.getItemById("{folder id}").upload(fileOptions);
+
+const uDriveIdRoot: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.upload(fileOptions);
+
+// file path includes folders
+const fileOptions2: IFileOptions = {
+    content: "This is some test content",
+    filePathName: "folderA/pnpTest.txt",
+    contentType: "text/plain;charset=utf-8"
+}
+
+const uFileOptions: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.upload(fileOptions2);
+
+

Add folder

+

Using addFolder you can add a folder

+
import { graph } from "@pnp/graph";
+import "@pnp/graph/onedrive";
+import "@pnp/graph/users"
+import {IDriveItemAddResult} from "@pnp/graph/ondrive";
+
+const graph = graphfi(...);
+
+const addFolder1: IDriveItemAddResult = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.children.addFolder('New Folder');
+const addFolder2: IDriveItemAddResult = await graph.me.drives.getById("{drive id}").root.children.addFolder('New Folder');
+
+
+

Search items

+

Using the search() you can search for items, and optionally select properties

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Where searchTerm is the query text used to search for items.
+// Values may be matched across several fields including filename, metadata, and file content.
+
+const search = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.search(searchTerm)();
+
+const search = await graph.me.drives.getById("{drive id}").root.search(searchTerm)();
+
+
+

Get specific item in drive

+

Using the items.getById() you can get a specific item from the current drive

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const item = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}")();
+
+const item = await graph.me.drives.getById("{drive id}").items.getById("{item id}")();
+
+
+

Get specific item in drive by path

+

Using the drive.getItemByPath() you can get a specific item from the current drive

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const item = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getItemByPath("MyFolder/MySubFolder/myFile.docx")();
+
+const item = await graph.me.drives.getItemByPath("MyFolder/MySubFolder/myFile.docx")();
+
+
+

Get drive item contents

+

Using the item.getContent() you can get the content of a file.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+private _readFileAsync(file: Blob): Promise<ArrayBuffer> {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = () => {
+      resolve(reader.result as ArrayBuffer);
+    };
+    reader.onerror = reject;
+    reader.readAsArrayBuffer(file);
+  });
+}
+
+// Where itemId is the id of the item
+const fileContents: Blob = await graph.me.drive.getItemById(itemId).getContent();
+const content: ArrayBuffer = await this._readFileAsync(fileContents);
+
+// This is an example of decoding plain text from the ArrayBuffer
+const decoder = new TextDecoder('utf-8');
+const decodedContent = decoder.decode(content);
+
+

Convert drive item contents

+

Using the item.convertContent() you can get a PDF version of the file. See official documentation for supported file types.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+private _readFileAsync(file: Blob): Promise<ArrayBuffer> {
+  return new Promise((resolve, reject) => {
+    const reader = new FileReader();
+    reader.onload = () => {
+      resolve(reader.result as ArrayBuffer);
+    };
+    reader.onerror = reject;
+    reader.readAsArrayBuffer(file);
+  });
+}
+
+// Where itemId is the id of the item
+const fileContents: Blob = await graph.me.drive.getItemById(itemId).convertContent("pdf");
+const content: ArrayBuffer = await this._readFileAsync(fileContents);
+
+// Further manipulation of the array buffer will be needed based on your requriements.
+
+

Get thumbnails

+

Using the thumbnails() you get the thumbnails

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const thumbs = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").thumbnails();
+
+const thumbs = await graph.me.drives.getById("{drive id}").items.getById("{item id}").thumbnails();
+
+
+

Delete drive item

+

Using the delete() you delete the current item

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const thumbs = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").delete();
+
+const thumbs = await graph.me.drives.getById("{drive id}").items.getById("{item id}").delete();
+
+
+

Update drive item metadata

+

Using the update() you update the current item

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+const update = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").update({name: "New Name"});
+
+const update = await graph.me.drives.getById("{drive id}").items.getById("{item id}").update({name: "New Name"});
+
+
+

Move drive item

+

Using the move() you move the current item, and optionally update it

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Requires a parentReference to the destination folder location
+const moveOptions: IItemOptions = {
+  parentReference: {
+    id?: {parentLocationId};
+    driveId?: {parentLocationDriveId}};
+  };
+  name?: {newName};
+};
+
+const move = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").move(moveOptions);
+
+const move = await graph.me.drives.getById("{drive id}").items.getById("{item id}").move(moveOptions);
+
+
+

Copy drive item

+

Using the copy() you can copy the current item to a new location, returns the path to the new location

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Requires a parentReference to the destination folder location
+const copyOptions: IItemOptions = {
+  parentReference: {
+    id?: {parentLocationId};
+    driveId?: {parentLocationDriveId}};
+  };
+  name?: {newName};
+};
+
+const copy = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").copy(copyOptions);
+
+const copy = await graph.me.drives.getById("{drive id}").items.getById("{item id}").copy(copyOptions);
+
+
+

Get the users special folders

+

Using the users default drive you can get special folders, including: Documents, Photos, CameraRoll, AppRoot, Music

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { SpecialFolder, IDriveItem } from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Get the special folder (App Root)
+const driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.AppRoot)();
+
+// Get the special folder (Documents)
+const driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.Documents)();
+
+// ETC
+
+

Get drive item preview

+

This action allows you to obtain a short-lived embeddable URL for an item in order to render a temporary preview.

+

If you want to obtain long-lived embeddable links, use the createLink API instead.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { IPreviewOptions, IDriveItemPreviewInfo } from "@pnp/graph/onedrive";
+import { ItemPreviewInfo } from "@microsoft/microsoft-graph-types"
+
+const graph = graphfi(...);
+
+const preview: ItemPreviewInfo = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").preview();
+
+const preview: ItemPreviewInfo = await graph.me.drives.getById("{drive id}").items.getById("{item id}").preview();
+
+const previewOptions: IPreviewOptions = {
+    page: 1,
+    zoom: 90
+}
+
+const preview2: ItemPreviewInfo = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").preview(previewOptions);
+
+
+

Track Changes

+

Track changes in a driveItem and its children over time.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { IDeltaItems } from "@pnp/graph/ondrive";
+
+const graph = graphfi(...);
+
+// Get the changes for the drive items from inception
+const delta: IDeltaItems = await graph.me.drive.root.delta()();
+const delta: IDeltaItems = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").root.delta()();
+
+// Get the changes for the drive items from token
+const delta: IDeltaItems = await graph.me.drive.root.delta("{token}")();
+
+

Get Drive Item Analytics

+

Using the analytics() you get the ItemAnalytics for a DriveItem

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/onedrive";
+import { IAnalyticsOptions } from "@pnp/graph/onedrive";
+
+const graph = graphfi(...);
+
+// Defaults to lastSevenDays
+const analytics = await graph.users.getById("user@tenant.onmicrosoft.com").drives.getById("{drive id}").items.getById("{item id}").analytics()();
+
+const analytics = await graph.me.drives.getById("{drive id}").items.getById("{item id}").analytics()();
+
+const analyticOptions: IAnalyticsOptions = {
+    timeRange: "allTime"
+};
+
+const analyticsAllTime = await graph.me.drives.getById("{drive id}").items.getById("{item id}").analytics(analyticOptions)();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/outlook/index.html b/graph/outlook/index.html new file mode 100644 index 000000000..cdd885d9d --- /dev/null +++ b/graph/outlook/index.html @@ -0,0 +1,2782 @@ + + + + + + + + + + + + + + + + + + + + + + + + outlook - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/outlook

+

Represents the Outlook services available to a user. Currently, only interacting with categories is supported.

+

You can learn more by reading the Official Microsoft Graph Documentation.

+

IUsers, IUser, IPeople

+

Invokable Banner Selective Imports Banner

+

Get All Categories User

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+
+const graph = graphfi(...);
+
+// Delegated permissions
+const categories = await graph.me.outlook.masterCategories();
+// Application permissions
+const categories = await graph.users.getById('{user id}').outlook.masterCategories();
+
+

Add Category User

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+
+const graph = graphfi(...);
+
+// Delegated permissions
+await graph.me.outlook.masterCategories.add({
+  displayName: 'Newsletters', 
+  color: 'preset2'
+});
+// Application permissions
+await graph.users.getById('{user id}').outlook.masterCategories.add({
+  displayName: 'Newsletters', 
+  color: 'preset2'
+});
+
+

Update Category

+

Known Issue Banner Testing has shown that displayName cannot be updated.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+import { OutlookCategory } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const categoryUpdate: OutlookCategory = {
+    color: "preset5"
+}
+
+// Delegated permissions
+const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate);
+// Application permissions
+const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);
+
+

Delete Category

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/outlook";
+
+const graph = graphfi(...);
+
+// Delegated permissions
+const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete();
+// Application permissions
+const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/photos/index.html b/graph/photos/index.html new file mode 100644 index 000000000..61007cf77 --- /dev/null +++ b/graph/photos/index.html @@ -0,0 +1,2876 @@ + + + + + + + + + + + + + + + + + + + + + + + + photos - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/photos

+

A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64.

+

You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.

+

IPhoto

+

Selective Imports Banner

+

Current User Photo

+

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.me.photo.getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+

Current User Photo by Size

+

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.me.photos.getBySize("48x48").getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+

Current Group Photo

+

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+

Current Group Photo by Size

+

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photos.getBySize("120x120").getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+

Current Team Photo

+

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const photoValue = await graph.teams.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.getBlob();
+const url = window.URL || window.webkitURL;
+const blobUrl = url.createObjectURL(photoValue);
+document.getElementById("photoElement").setAttribute("src", blobUrl);
+
+

Set User Photo

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const input = <HTMLInputElement>document.getElementById("thefileinput");
+const file = input.files[0];
+await graph.me.photo.setContent(file);
+
+

Set Group Photo

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const input = <HTMLInputElement>document.getElementById("thefileinput");
+const file = input.files[0];
+await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.setContent(file);
+
+

Set Team Photo

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const input = <HTMLInputElement>document.getElementById("thefileinput");
+const file = input.files[0];
+await graph.teams.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.setContent(file);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/planner/index.html b/graph/planner/index.html new file mode 100644 index 000000000..99f3e5c0c --- /dev/null +++ b/graph/planner/index.html @@ -0,0 +1,3126 @@ + + + + + + + + + + + + + + + + + + + + + + + + planner - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/planner

+

The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can add, update and delete items in Planner.

+

IInvitations

+

Invokable Banner Selective Imports Banner

+

Get Plans by Id

+

Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const plan = await graph.planner.plans.getById('planId')();
+
+
+

Add new Plan

+

Using the planner.plans.add() you can create a new Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const newPlan = await graph.planner.plans.add('groupObjectId', 'title');
+
+
+

Get Tasks in Plan

+

Using the tasks() you can get the Tasks in a Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planTasks = await graph.planner.plans.getById('planId').tasks();
+
+
+

Get Buckets in Plan

+

Using the buckets() you can get the Buckets in a Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planBuckets = await graph.planner.plans.getById('planId').buckets();
+
+
+

Get Details in Plan

+

Using the details() you can get the details in a Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planDetails = await graph.planner.plans.getById('planId').details();
+
+
+

Delete Plan

+

Using the delete() you can get delete a Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const delPlan = await graph.planner.plans.getById('planId').delete('planEtag');
+
+
+

Update Plan

+

Using the update() you can get update a Plan.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});
+
+
+

Get All My Tasks from all plans

+

Using the tasks() you can get the Tasks across all plans

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const planTasks = await graph.me.tasks()
+
+
+

Get Task by Id

+

Using the planner.tasks.getById() you can get a specific Task. +Planner.tasks is not an available endpoint, you need to get a specific Task.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const task = await graph.planner.tasks.getById('taskId')();
+
+
+

Add new Task

+

Using the planner.tasks.add() you can create a new Task.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const newTask = await graph.planner.tasks.add('planId', 'title');
+
+
+

Get Details in Task

+

Using the details() you can get the details in a Task.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const taskDetails = await graph.planner.tasks.getById('taskId').details();
+
+
+

Delete Task

+

Using the delete() you can get delete a Task.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');
+
+
+

Update Task

+

Using the update() you can get update a Task.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});
+
+
+

Get Buckets by Id

+

Using the planner.buckets.getById() you can get a specific Bucket. +planner.buckets is not an available endpoint, you need to get a specific Bucket.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const bucket = await graph.planner.buckets.getById('bucketId')();
+
+
+

Add new Bucket

+

Using the planner.buckets.add() you can create a new Bucket.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const newBucket = await graph.planner.buckets.add('name', 'planId');
+
+
+

Update Bucket

+

Using the update() you can get update a Bucket.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const updBucket = await graph.planner.buckets.getById('bucketId').update({name: "Name", eTag:'bucketEtag'});
+
+
+

Delete Bucket

+

Using the delete() you can get delete a Bucket.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');
+
+
+

Get Bucket Tasks

+

Using the tasks() you can get Tasks in a Bucket.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();
+
+
+

Get Plans for a group

+

Gets all the plans for a group

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/groups";
+import "@pnp/graph/planner";
+
+const graph = graphfi(...);
+
+const plans = await graph.groups.getById("b179a282-9f94-4bb5-a395-2a80de5a5a78").plans();
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/search/index.html b/graph/search/index.html new file mode 100644 index 000000000..c85b9d689 --- /dev/null +++ b/graph/search/index.html @@ -0,0 +1,2681 @@ + + + + + + + + + + + + + + + + + + + + + + + + search - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/search

+

The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below.

+

Selective Imports Banner

+

Call graph.query

+

This example shows calling the search API via the query method of the root graph object.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/search";
+
+const graph = graphfi(...);
+
+const results = await graph.query({
+    entityTypes: ["site"],
+    query: {
+        queryString: "test"
+    },
+});
+
+
+

Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/shares/index.html b/graph/shares/index.html new file mode 100644 index 000000000..5135476bb --- /dev/null +++ b/graph/shares/index.html @@ -0,0 +1,2720 @@ + + + + + + + + + + + + + + + + + + + + + + + + shares - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/shares

+

The shares module allows you to access shared files, or any file in the tenant using encoded file urls.

+

Selective Imports Banner

+

Access a Share by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/shares";
+
+const graph = graphfi(...);
+
+const shareInfo = await graph.shares.getById("{shareId}")();
+
+ +

If you don't have a share id but have the absolute path to a file you can encode it into a sharing link, allowing you to access it directly using the /shares endpoint.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/shares";
+
+const graph = graphfi(...);
+
+const shareLink: string = graph.shares.encodeSharingLink("https://{tenant}.sharepoint.com/sites/dev/Shared%20Documents/new.pptx");
+
+const shareInfo = await graph.shares.getById(shareLink)();
+
+

Access a Share's driveItem resource

+

You can also access the full functionality of the driveItem via a share. Find more details on the capabilities of driveItem here.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/shares";
+
+const graph = graphfi(...);
+
+const driveItemInfo = await graph.shares.getById("{shareId}").driveItem();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/sites/index.html b/graph/sites/index.html new file mode 100644 index 000000000..b9b17ccb6 --- /dev/null +++ b/graph/sites/index.html @@ -0,0 +1,2735 @@ + + + + + + + + + + + + + + + + + + + + + + + + sites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/sites

+

The search module allows you to access the Microsoft Graph Sites API.

+

Selective Imports Banner

+

Call graph.sites

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+
+const graph = graphfi(...);
+
+const sitesInfo = await graph.sites();
+
+

Call graph.sites.getById

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+
+const graph = graphfi(...);
+
+const siteInfo = await graph.sites.getById("{site identifier}")();
+
+

Call graph.sites.getByUrl

+

Using the sites.getByUrl() you can get a site using url instead of identifier

+

Known Issue Banner If you get a site with this method, the graph does not support chaining a request further than .drive. We will review and try and create a work around for this issue.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/sites";
+
+const graph = graphfi(...);
+const sharepointHostName = "contoso.sharepoint.com";
+const serverRelativeUrl = "/sites/teamsite1";
+const siteInfo = await graph.sites.getByUrl(sharepointHostName, serverRelativeUrl)();
+
+

Make additional calls or recieve items from lists

+

We don't currently implement all of the available options in graph for sites, rather focusing on the sp library. While we do accept PRs to add functionality, you can also make calls by path.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/subscriptions/index.html b/graph/subscriptions/index.html new file mode 100644 index 000000000..46e2be86c --- /dev/null +++ b/graph/subscriptions/index.html @@ -0,0 +1,2776 @@ + + + + + + + + + + + + + + + + + + + + + + + + subscriptions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/graph/subscriptions

+

The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft graph. Currently, subscriptions are enabled for the following resources:

+
    +
  • Mail, events, and contacts from Outlook.
  • +
  • Conversations from Office Groups.
  • +
  • Drive root items from OneDrive.
  • +
  • Users and Groups from Azure Active Directory.
  • +
  • Alerts from the Microsoft Graph Security API.
  • +
+

Get all of the Subscriptions

+

Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const subscriptions = await graph.subscriptions();
+
+
+

Create a new Subscription

+

Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const addedSubscription = await graph.subscriptions.add("created,updated", "https://webhook.azurewebsites.net/api/send/myNotifyClient", "me/mailFolders('Inbox')/messages", "2019-11-20T18:23:45.9356913Z");
+
+
+

Get Subscription by Id

+

Using the subscriptions.getById() you can get one of the subscriptions

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const subscription = await graph.subscriptions.getById('subscriptionId')();
+
+
+

Delete a Subscription

+

Using the subscriptions.getById().delete() you can remove one of the Subscriptions

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const delSubscription = await graph.subscriptions.getById('subscriptionId').delete();
+
+
+

Update a Subscription

+

Using the subscriptions.getById().update() you can update one of the Subscriptions

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/subscriptions";
+
+const graph = graphfi(...);
+
+const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: "created,updated,deleted" });
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/teams/index.html b/graph/teams/index.html new file mode 100644 index 000000000..90fa4be2d --- /dev/null +++ b/graph/teams/index.html @@ -0,0 +1,3228 @@ + + + + + + + + + + + + + + + + + + + + + + + + teams - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/teams

+

The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described +you can add, update and delete items in Teams.

+

Teams the user is a member of

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams();
+
+const myJoinedTeams = await graph.me.joinedTeams();
+
+
+

Get Teams by Id

+

Using the teams.getById() you can get a specific Team.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();
+
+

Create new Team/Group - Method #1

+

The first way to create a new Team and corresponding Group is to first create the group and then create the team. +Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.

+

Create a Team via a specific group

+

Here we get the group via id and use createTeam

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import "@pnp/graph/groups";
+
+const graph = graphfi(...);
+
+const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({
+"memberSettings": {
+    "allowCreateUpdateChannels": true
+},
+"messagingSettings": {
+        "allowUserEditMessages": true,
+"allowUserDeleteMessages": true
+},
+"funSettings": {
+    "allowGiphy": true,
+    "giphyContentRating": "strict"
+}});
+
+

Create new Team/Group - Method #2

+

The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const team = {
+        "template@odata.bind": "https://graph.microsoft.com/v1.0/teamsTemplates('standard')",
+        "displayName": "PnPJS Test Team",
+        "description": "PnPJS Test Team’s Description",
+        "members": [
+            {
+                "@odata.type": "#microsoft.graph.aadUserConversationMember",
+                "roles": ["owner"],
+                "user@odata.bind": "https://graph.microsoft.com/v1.0/users('{owners user id}')",
+            },
+        ],
+    };
+
+const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team);
+//To check the status of the team creation, call getOperationById for the newly created team.
+const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);
+
+

Clone a Team

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
+'Cloned','description','apps,tabs,settings,channels,members','public');
+
+
+

Get Teams Async Operation

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
+'Cloned','description','apps,tabs,settings,channels,members','public');
+const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);
+
+

Archive a Team

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();
+
+

Unarchive a Team

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();
+
+

Get all channels of a Team

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();
+
+

Get primary channel

+

Using the teams.getById() you can get a specific Team.

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').primaryChannel();
+
+

Get channel by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();
+
+
+

Create a new Channel

+
import { graphfi } from "@pnp/graph";
+
+const graph = graphfi(...);
+
+const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');
+
+
+

List Messages

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const chatMessages = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages();
+
+

Add chat message to Channel

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+import { ChatMessage } from "@microsoft/microsoft-graph-types";
+
+const graph = graphfi(...);
+
+const message = {
+      "body": {
+        "content": "Hello World"
+      }
+    }
+const chatMessage: ChatMessage = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages.add(message);
+
+

Get installed Apps

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();
+
+
+

Add an App

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');
+
+
+

Remove an App

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.delete();
+
+
+

Get Tabs from a Channel

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();
+
+
+

Get Tab by Id

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();
+
+
+

Add a new Tab to Channel

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{});
+
+
+

Update a Tab

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').update({
+    displayName: "New tab name"
+});
+
+
+

Remove a Tab from channel

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/teams";
+
+const graph = graphfi(...);
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').delete();
+
+
+

Team Membership

+

Get the members and/or owners of a group.

+

See Groups

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/graph/users/index.html b/graph/users/index.html new file mode 100644 index 000000000..a2536c153 --- /dev/null +++ b/graph/users/index.html @@ -0,0 +1,2939 @@ + + + + + + + + + + + + + + + + + + + + + + + + users - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/graph/users

+

Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services.

+

You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.

+

IUsers, IUser, IPeople

+

Invokable Banner Selective Imports Banner

+

Current User

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const currentUser = await graph.me();
+
+

Get Users in the Organization

+
+

If you want to get all users you will need to use paging

+
+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const allUsers = await graph.users();
+
+

Get a User by email address (or user id)

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const matchingUser = await graph.users.getById('jane@contoso.com')();
+
+

User Properties

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+await graph.me.memberOf();
+await graph.me.transitiveMemberOf();
+
+

Update Current User

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+await graph.me.update({
+    displayName: 'John Doe'
+});
+
+

People

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const people = await graph.me.people();
+
+// get the top 3 people
+const people = await graph.me.people.top(3)();
+
+

Manager

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const manager = await graph.me.manager();
+
+

Direct Reports

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+
+const graph = graphfi(...);
+
+const reports = await graph.me.directReports();
+
+

Photo

+
import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users";
+import "@pnp/graph/photos";
+
+const graph = graphfi(...);
+
+const currentUser = await graph.me.photo();
+const specificUser = await graph.users.getById('jane@contoso.com').photo();
+
+

User Photo Operations

+

See Photos

+

User Presence Operation

+

See Cloud Communications

+

User Messages (Mail)

+

See Messages

+

User OneDrive

+

See OneDrive

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/img/ConsoleListenerColors.png b/img/ConsoleListenerColors.png new file mode 100644 index 000000000..0b07afbf7 Binary files /dev/null and b/img/ConsoleListenerColors.png differ diff --git a/img/Logo.png b/img/Logo.png new file mode 100644 index 000000000..73dc570b3 Binary files /dev/null and b/img/Logo.png differ diff --git a/img/PnPJS_FluentAPI.gif b/img/PnPJS_FluentAPI.gif new file mode 100644 index 000000000..e65e84fba Binary files /dev/null and b/img/PnPJS_FluentAPI.gif differ diff --git a/img/SPFx-On-Premesis-2016-1.png b/img/SPFx-On-Premesis-2016-1.png new file mode 100644 index 000000000..9ea42e8dd Binary files /dev/null and b/img/SPFx-On-Premesis-2016-1.png differ diff --git a/img/TimelineArchitecture.jpg b/img/TimelineArchitecture.jpg new file mode 100644 index 000000000..e4c24627a Binary files /dev/null and b/img/TimelineArchitecture.jpg differ diff --git a/img/csp_copyccvalue.png b/img/csp_copyccvalue.png new file mode 100644 index 000000000..dd1aa7394 Binary files /dev/null and b/img/csp_copyccvalue.png differ diff --git a/img/csp_networktab.png b/img/csp_networktab.png new file mode 100644 index 000000000..819d7f999 Binary files /dev/null and b/img/csp_networktab.png differ diff --git a/img/office365-header-icon.png b/img/office365-header-icon.png new file mode 100644 index 000000000..529191ea6 Binary files /dev/null and b/img/office365-header-icon.png differ diff --git a/img/usage-2020-eoy.png b/img/usage-2020-eoy.png new file mode 100644 index 000000000..3f89f8022 Binary files /dev/null and b/img/usage-2020-eoy.png differ diff --git a/img/usage-2021-eoy.png b/img/usage-2021-eoy.png new file mode 100644 index 000000000..c4f8d4499 Binary files /dev/null and b/img/usage-2021-eoy.png differ diff --git a/img/usage-2022-eoy.png b/img/usage-2022-eoy.png new file mode 100644 index 000000000..6b84748ba Binary files /dev/null and b/img/usage-2022-eoy.png differ diff --git a/index.html b/index.html new file mode 100644 index 000000000..effd97059 --- /dev/null +++ b/index.html @@ -0,0 +1,2833 @@ + + + + + + + + + + + + + + + + + + + + + + PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Home

+ +

SharePoint Patterns and Practices Logo

+

PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.

+

These articles provide general guidance for working with the libraries. If you are migrating from V2 please review the transition guide.

+ +

Fluent API in action

+

Animation of the library in use, note intellisense help in building your queries

+

Packages

+

Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.

+

The latest published version is npm version.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@pnp/
azidjsclientProvides an Azure Identity wrapper suitable for use with PnPjs
coreProvides shared functionality across all pnp libraries
graphProvides a fluent api for working with Microsoft Graph
loggingLight-weight, subscribable logging framework
msaljsclientProvides an msal wrapper suitable for use with PnPjs
nodejsProvides functionality enabling the @pnp libraries within nodejs
queryableProvides shared query functionality and base classes
spProvides a fluent api for working with SharePoint REST
sp-adminProvides a fluent api for working with M365 Tenant admin methods
+

Authentication

+

We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!

+

Issues, Questions, Ideas

+

Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.

+

Changelog

+

Please review the CHANGELOG for release details on all library changes.

+

Code of Conduct

+

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

+

"Sharing is Caring"

+

Please use http://aka.ms/community/home for the latest updates around the whole Microsoft 365 and Power Platform Community(PnP) initiative.

+

Disclaimer

+

THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/logging/index.html b/logging/index.html new file mode 100644 index 000000000..6daead3b5 --- /dev/null +++ b/logging/index.html @@ -0,0 +1,3091 @@ + + + + + + + + + + + + + + + + + + + + + + + + logging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/logging

+

npm version

+

The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.

+

Getting Started

+

Install the logging module, it has no other dependencies

+

npm install @pnp/logging --save

+

Understanding the Logging Framework

+

The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter.

+
/**
+ * Interface that defines a log listener
+ *
+ */
+export interface ILogListener {
+    /**
+     * Any associated data that a given logging listener may choose to log or ignore
+     *
+     * @param entry The information to be logged
+     */
+    log(entry: ILogEntry): void;
+}
+
+/**
+ * Interface that defines a log entry
+ *
+ */
+export interface ILogEntry {
+    /**
+     * The main message to be logged
+     */
+    message: string;
+    /**
+     * The level of information this message represents
+     */
+    level: LogLevel;
+    /**
+     * Any associated data that a given logging listener may choose to log or ignore
+     */
+    data?: any;
+}
+
+

Log Levels

+
export const enum LogLevel {
+    Verbose = 0,
+    Info = 1,
+    Warning = 2,
+    Error = 3,
+    Off = 99,
+}
+
+

Writing to the Logger

+

To write information to a logger you can use either write, writeJSON, or log.

+
import {
+    Logger,
+    LogLevel
+} from "@pnp/logging";
+
+// write logs a simple string as the message value of the LogEntry
+Logger.write("This is logging a simple string");
+
+// optionally passing a level, default level is Verbose
+Logger.write("This is logging a simple string", LogLevel.Error);
+
+// this will convert the object to a string using JSON.stringify and set the message with the result
+Logger.writeJSON({ name: "value", name2: "value2"});
+
+// optionally passing a level, default level is Verbose
+Logger.writeJSON({ name: "value", name2: "value2"}, LogLevel.Warning);
+
+// specify the entire LogEntry interface using log
+Logger.log({
+    data: { name: "value", name2: "value2"},
+    level: LogLevel.Warning,
+    message: "This is my message"
+});
+
+

Log an error

+

There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error +instance passed in, the level will be 'Error', and the message will be the Error instance's message property.

+
const e = Error("An Error");
+
+Logger.error(e);
+
+

Subscribing a Listener

+

By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.

+
import {
+    Logger,
+    ConsoleListener,
+    LogLevel
+} from "@pnp/logging";
+
+// subscribe a listener
+Logger.subscribe(ConsoleListener());
+
+// set the active log level
+Logger.activeLogLevel = LogLevel.Info;
+
+

Available Listeners

+

There are two listeners included in the library, ConsoleListener and FunctionListener.

+

ConsoleListener

+

This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.

+

Configuration Options

+

Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).

+
Using a Prefix
+

To add a prefix to all output, supply a string in the constructor:

+
import {
+    Logger,
+    ConsoleListener,
+    LogLevel
+} from "@pnp/logging";
+
+const LOG_SOURCE: string = 'MyAwesomeWebPart';
+Logger.subscribe(ConsoleListener(LOG_SOURCE));
+Logger.activeLogLevel = LogLevel.Info;
+
+

With the above configuration, Logger.write("My special message"); will be output to the console as:

+
MyAwesomeWebPart - My special message
+
+
Customizing Text Color
+

You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color).

+

Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.):

+
import {
+    Logger,
+    ConsoleListener,
+    LogLevel
+} from "@pnp/logging";
+
+const LOG_SOURCE: string = 'MyAwesomeWebPart';
+Logger.subscribe(ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'}));
+Logger.activeLogLevel = LogLevel.Info;
+
+

With the above configuration:

+
Logger.write("My special message");
+Logger.write("A warning!", LogLevel.Warning);
+
+

Will result in messages that look like this:

+

ConsoleListener with Colors

+

Color options:

+
    +
  • color: Default text color for all logging levels unless they're specified
  • +
  • verboseColor: Text color to use for messages with LogLevel.Verbose
  • +
  • infoColor: Text color to use for messages with LogLevel.Info
  • +
  • warningColor: Text color to use for messages with LogLevel.Warning
  • +
  • errorColor: Text color to use for messages with LogLevel.Error
  • +
+

To set colors without a prefix, specify either undefined or an empty string for the first parameter:

+
Logger.subscribe(ConsoleListener(undefined, {color:'purple'}));
+
+

FunctionListener

+

The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.

+
import {
+    Logger,
+    FunctionListener,
+    ILogEntry
+} from "@pnp/logging";
+
+let listener = new FunctionListener((entry: ILogEntry) => {
+
+    // pass all logging data to an existing framework
+    MyExistingCompanyLoggingFramework.log(entry.message);
+});
+
+Logger.subscribe(listener);
+
+

Create a Custom Listener

+

If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface.

+
import {
+    Logger,
+    ILogListener,
+    ILogEntry
+} from "@pnp/logging";
+
+class MyListener implements ILogListener {
+
+    log(entry: ILogEntry): void {
+        // here you would do something with the entry
+    }
+}
+
+Logger.subscribe(new MyListener());
+
+

Logging Behavior

+

To allow seamless logging with v3 we have introduced the PnPLogging behavior. It takes a single augument representing the log level of that behavior, allowing you to be very selective in what logging you want to get. As well the log level applied here ignores any global level set with activeLogLevel on Logger.

+
import { LogLevel, PnPLogging, Logger, ConsoleListener } from "@pnp/logging";
+import { spfi, SPFx } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+// subscribe a listener
+Logger.subscribe(ConsoleListener());
+
+// at the root we only want to log errors, which will be sent to all subscribed loggers on Logger
+const sp = spfi().using(SPFx(this.context), PnPLogging(LogLevel.Error));
+
+
+const list = sp.web.lists.getByTitle("My List");
+// use verbose logging with this particular list because you are trying to debug something
+list.using(PnPLogging(LogLevel.Verbose));
+
+const listData = await list();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/msaljsclient/index.html b/msaljsclient/index.html new file mode 100644 index 000000000..19574fdd3 --- /dev/null +++ b/msaljsclient/index.html @@ -0,0 +1,2640 @@ + + + + + + + + + + + + + + + + + + + + + + + + msaljsclient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/msaljsclient

+

This library provides a thin wrapper around the msal library to make it easy to integrate MSAL authentication in the browser.

+

You will first need to install the package:

+

npm install @pnp/msaljsclient --save

+

The configuration and authParams

+
import { spfi, SPBrowser } from "@pnp/sp";
+import { MSAL } from "@pnp/msaljsclient";
+import "@pnp/sp/webs";
+
+const configuation = {
+    auth: {
+        authority: "https://login.microsoftonline.com/common",
+        clientId: "{client id}",
+    }
+};
+
+const authParams = {
+    scopes: ["https://{tenant}.sharepoint.com/.default"],
+};
+
+const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(configuration, authParams));
+
+const webData = await sp.web();
+
+

Please see more scenarios in the authentication article.

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/news/2020-year-in-review/index.html b/news/2020-year-in-review/index.html new file mode 100644 index 000000000..835114de4 --- /dev/null +++ b/news/2020-year-in-review/index.html @@ -0,0 +1,2960 @@ + + + + + + + + + + + + + + + + + + + + + + + + 2020 - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

2020 Year End Report

+

Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.

+

This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules.

+

We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community.

+

Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured "roots" such as "sp" or "graph" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios.

+

Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.

+

Usage

+

In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227.

+

These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November.

+

Graph showing requests and tenants/month for @pnp/sp

+
+

1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds

+
+

Releases

+

We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

+

NPM Package download statistics (@pnp/sp):

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MonthCount*MonthCount
January100,686*July36,805
February34,437*August38,897
March34,574*September45,968
April32,436*October46,655
May34,482*November45,511
June34,408*December58,977
Grand Total543,836
+

With 2020 our total all time downloads of @pnp/sp is now at: 949,638

+
+

Stats from https://npm-stat.com/

+
+

Future Plans

+

Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date.

+

Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.

+

New Lead Maintainer

+

With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work.

+

Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean.

+

We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.

+

Contributors

+

As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.

+

+ AJIXuMuK + + Ashikpaul + + cesarhoeflich + + dcashpeterson + + dependabot[bot] + + derhallim + + DRamalho92 + + f1nzer + + Harshagracy + + holylander + + hugoabernier + + JakeStanger + + jaywellings + + JMTeamway + + joelfmrodrigues + + juliemturner + + jusper-dk + + KEMiCZA + + koltyakov + + kunj-sangani + + MarkyDeParky + + mikezimm + + mrebuffet + + naugtur + + NZainchkovskiy + + PaoloPia + + patrick-rodgers + + ravichandran-blog + + RoelVB + + siddharth-vaghasia + + simonagren + + tavikukko + + ValerasNarbutas +

+

Sponsors

+

We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

+

Thank You

+

+ KEMiCZA + + Sympraxis Consulting + + thechriskent + + erwinvanhunen + + PopWarner + + VesaJuvonen + + LauraKokkarinen + + ricardocarneiro + + andrewconnell +

+

Closing

+

In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

+

Wishing you the very best for 2021,

+

The PnPjs Team

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/news/2021-year-in-review/index.html b/news/2021-year-in-review/index.html new file mode 100644 index 000000000..ef874b60b --- /dev/null +++ b/news/2021-year-in-review/index.html @@ -0,0 +1,2937 @@ + + + + + + + + + + + + + + + + + + + + + + + + 2021 - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

2021 Year End Report

+

Welcome to our second year in review report for PnPjs. 2021 found us planning, building, testing, and documenting a whole new version of PnPjs. The goal is to deliver a much improved and flexible experience and none of that would have been possible without the support and participation of everyone in the PnP community - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.

+

Because of the huge useage we've seen with the library and issues we found implementing some of the much requested enhancements, we felt we really needed to start from the ground up and rearchitect the library completely. This new design, built on the concept of a "Timeline", enabled us to build a significantly lighter weight solution that is more extensible than ever. And bonus, we were able to keep the overall development experience largly unchanged, so that makes transitioning all that much easier. In addition we took extra effort to validate our development efforts by making sure all our tests passed so that we could better ensure quality of the library. Check out our Transition Guide and ChangeLog for all the details.

+

In other news, we fixed 47 reported bugs, answered 89 questions, and made 51 suggested enhancements to version 2 of the library - all driven by feedback from users and the community.

+

Usage

+

In 2021 we transitioned from rapid growth to slower growth but maintaining a request/month rate over 11 billion, approaching 13 billion by the end of the year. These requests came from more than 25 thousand tenants including some of the largest M365 customers. Due to some data cleanup we don't have the full year's information, but the below graph shows the final 7 months of the year.

+

Graph showing requests and tenants/month for @pnp/sp

+

Releases

+

We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

+

NPM Package download statistics (@pnp/sp)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MonthCount*MonthCount
January49,446*July73,491
February56,054*August74,236
March66,113*September69,179
April58,526*October77,645
May62,747*November74,966
June69,349*December61,995
Grand Total793,747
+

For comparison our total downloads in 2020 was 543,836.

+

With 2021 our total all time downloads of @pnp/sp is now at: 1,743,385

+

In 2020 the all time total was 949,638.

+
+

Stats from https://npm-stat.com/

+
+

Future Plans

+

Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.

+

Contributors

+

As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.

+

+ AJIXuMuK + + Ashikpaul + + cesarhoeflich + + dcashpeterson + + dependabot[bot] + + derhallim + + DRamalho92 + + f1nzer + + Harshagracy + + holylander + + hugoabernier + + JakeStanger + + jaywellings + + JMTeamway + + joelfmrodrigues + + juliemturner + + jusper-dk + + KEMiCZA + + koltyakov + + kunj-sangani + + MarkyDeParky + + mikezimm + + mrebuffet + + naugtur + + NZainchkovskiy + + PaoloPia + + patrick-rodgers + + ravichandran-blog + + RoelVB + + siddharth-vaghasia + + simonagren + + tavikukko + + ValerasNarbutas +

+

Sponsors

+

We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

+

Thank You

+

+ KEMiCZA + + Sympraxis Consulting + + thechriskent + + erwinvanhunen + + PopWarner + + VesaJuvonen + + LauraKokkarinen + + ricardocarneiro + + andrewconnell +

+

Closing

+

In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

+

Wishing you the very best for 2022,

+

The PnPjs Team

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/news/2022-year-in-review/index.html b/news/2022-year-in-review/index.html new file mode 100644 index 000000000..6ba5f48a6 --- /dev/null +++ b/news/2022-year-in-review/index.html @@ -0,0 +1,2909 @@ + + + + + + + + + + + + + + + + + + + + + + + + 2022 - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

2022 Year End Report

+

Wow, what a year for PnPjs! We released our latest major version 3.0 on Valentine's Day 2022 which included significant performance improvements, a completely rewritten internal architecture, and reduced the bundled library size by two-thirds. As well we continued out monthly releases bringing enhancements and bug fixes to our users on a continual basis.

+

But before we go any further we once again say Thank You!!! to everyone that has used, contributed to, and provided feedback on the library. This journey is not possible without you, and this last year you have driven us to be our best.

+

Version 3 introduces a completely new design for the internals of the library, easily allowing consumers to customize any part of the request process to their needs. Centered around an extensible Timeline and extended for http requests by Queryable this new pattern reduced code duplication, interlock, and complexity significantly. It allows everything in the request flow to be controlled through behaviors, which are plain functions acting at the various stages of the request. Using this model we reimagined batching, caching, authentication, and parsing in simpler, composable ways. If you have not yet updated to version 3, we encourage you to do so. You can review the transition guide to get started.

+

As one last treat, we set up nightly builds so that each day you can get a fresh version with any updates merged the previous day. This is super helpful if you're waiting for a specific fix or feature for your project. It allows for easier testing of new features through the full dev lifecycle, as well.

+

In other news, we fixed 54 reported bugs, answered 123 questions, and made 54 suggested enhancements to version 3 of the library - all driven by feedback from users and the community.

+

Usage

+

In 2022 we continued to see steady usage and growth maintaining a requst/month rate over 30 billion for much of the year. These requets came from ~29K tenants a month, including some of our largest M365 customers.

+

Graph showing requests and tenants/month for @pnp/sp

+

Releases

+

We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

+

NPM Package download statistics (@pnp/sp)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
MonthCount*MonthCount
January70,863*July63,844
February76,649*August75,713
March83,902*September71,447
April70,429*October84,744
May72,406*November82,459
June71,375*December65,785
Grand Total889,616
+

For comparison our total downloads in 2021 was 793,747.

+

With 2022 our total all time downloads of @pnp/sp is now at: 2,543,639

+

In 2021 the all time total was 1,743,385.

+
+

Stats from https://npm-stat.com/

+
+

Future Plans

+

Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.

+

Contributors

+

As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2021! If you are interested in becoming a contributor check out our guide on ways to get started.

+

+ juliemturner + + tavikukko + + michael-ra + + dylanbr0wn + + wilecoyotegenius + + wmertens + + Taaqif + + aaademosu + + martinlingstuyl + + Saintenr + + sympmarc + + DmitriyVdE + + milanholemans + + amartyadav + + andreasmarkussen + + LuiseFreese + + SuperioOne + + waldekmastykarz + + robert-lindstrom + + JakeStanger +

+

Sponsors

+

We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

+

Thank You

+

+ KEMiCZA + + Sympraxis Consulting + + thechriskent + + erwinvanhunen + + PopWarner + + jansenbe + + YannickRe +

+

Closing

+

In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

+

Wishing you the very best for 2023,

+

The PnPjs Team

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/nodejs/behaviors/index.html b/nodejs/behaviors/index.html new file mode 100644 index 000000000..f688eb0fe --- /dev/null +++ b/nodejs/behaviors/index.html @@ -0,0 +1,2858 @@ + + + + + + + + + + + + + + + + + + + + + + + + behaviors - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/nodejs : behaviors

+

The article describes the behaviors exported by the @pnp/nodejs library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/graph.

+

NodeFetch

+

This behavior, for use in nodejs, provides basic fetch support through the node-fetch package. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing

+
+

For fetch configuration in browsers please see @pnp/queryable behaviors.

+
+
import { NodeFetch } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetch());
+
+await sp.webs();
+
+
import { NodeFetch } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetch({ replace: false }));
+
+await sp.webs();
+
+

NodeFetchWithRetry

+

This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.

+
import { NodeFetchWithRetry } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetchWithRetry());
+
+await sp.webs();
+
+

You can also control how the behavior works through its props. The replace value works as described above for NodeFetch. interval specifies the initial dynamic back off value in milliseconds. This value is ignored if a "Retry-After" header exists in the response. retries indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.

+
import { NodeFetchWithRetry } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(NodeFetchWithRetry({
+    retries: 2,
+    interval: 400,
+    replace: true,
+}));
+
+await sp.webs();
+
+

GraphDefault

+

The GraphDefault behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse, graph's DefaultHeaders, and graph's DefaultInit. It is configured using a props argument:

+
interface IGraphDefaultProps {
+    baseUrl?: string;
+    msal: {
+        config: Configuration;
+        scopes?: string[];
+    };
+}
+
+

You can use the baseUrl property to specify either v1.0 or beta - or one of the special graph urls.

+
import { GraphDefault } from "@pnp/nodejs";
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users/index.js";
+
+const graph = graphfi().using(GraphDefault({
+    // use the German national graph endpoint
+    baseUrl: "https://graph.microsoft.de/v1.0",
+    msal: {
+        config: { /* my msal config */ },
+    }
+}));
+
+await graph.me();
+
+

MSAL

+

This behavior provides a thin wrapper around the @azure/msal-node library. The options you provide are passed directly to msal, and all options are available.

+
import { MSAL } from "@pnp/nodejs";
+import { graphfi } from "@pnp/graph";
+import "@pnp/graph/users/index.js";
+
+const graph = graphfi().using(MSAL(config: { /* my msal config */ }, scopes: ["https://graph.microsoft.com/.default"]);
+
+await graph.me();
+
+

SPDefault

+

The SPDefault behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse,sp's DefaultHeaders, and sp's DefaultInit. It is configured using a props argument:

+
interface ISPDefaultProps {
+    baseUrl?: string;
+    msal: {
+        config: Configuration;
+        scopes: string[];
+    };
+}
+
+

You can use the baseUrl property to specify the absolute site/web url to which queries should be set.

+
import { SPDefault } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(SPDefault({
+    msal: {
+        config: { /* my msal config */ },
+        scopes: ["Scope.Value", "Scope2.Value"],
+    }
+}));
+
+await sp.web();
+
+

StreamParse

+

StreamParse is a specialized parser allowing request results to be read as a nodejs stream. The return value when using this parser will be of the shape:

+
{
+    body: /* The .body property of the Response object */,
+    knownLength: /* number value calculated from the Response's content-length header */
+}
+
+
import { StreamParse } from "@pnp/nodejs";
+
+import "@pnp/sp/webs/index.js";
+
+const sp = spfi().using(StreamParse());
+
+const streamResult = await sp.someQueryThatReturnsALargeFile();
+
+// read the stream as text
+const txt = await new Promise<string>((resolve) => {
+    let data = "";
+    streamResult.body.on("data", (chunk) => data += chunk);
+    streamResult.body.on("end", () => resolve(data));
+});
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/nodejs/sp-extensions/index.html b/nodejs/sp-extensions/index.html new file mode 100644 index 000000000..6066e7589 --- /dev/null +++ b/nodejs/sp-extensions/index.html @@ -0,0 +1,2797 @@ + + + + + + + + + + + + + + + + + + + + + + + + sp Extensions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/nodejs - sp extensions

+

By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api.

+

IFile.getStream

+

Allows you to read a response body as a nodejs PassThrough stream.

+
// by importing the the library the node specific extensions are automatically applied
+import { SPDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+
+const sp = spfi("https://something.com").using(SPDefault({
+    // config
+}));
+
+// get the stream
+const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl("/sites/dev/file.txt").getStream();
+
+// see if we have a known length
+console.log(streamResult.knownLength);
+
+// read the stream
+// this is a very basic example - you can do tons more with streams in node
+const txt = await new Promise<string>((resolve) => {
+    let data = "";
+    stream.body.on("data", (chunk) => data += chunk);
+    stream.body.on("end", () => resolve(data));
+});
+
+

IFiles.addChunked

+
import { SPDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs/index.js";
+import "@pnp/sp/folders/web.js";
+import "@pnp/sp/folders/list.js";
+import "@pnp/sp/files/web.js";
+import "@pnp/sp/files/folder.js";
+import * as fs from "fs";
+
+const sp = spfi("https://something.com").using(SPDefault({
+    // config
+}));
+
+// NOTE: you must supply the highWaterMark to determine the block size for stream uploads
+const stream = fs.createReadStream("{file path}", { highWaterMark: 10485760 });
+const files = sp.web.defaultDocumentLibrary.rootFolder.files;
+
+// passing the chunkSize parameter has no affect when using a stream, use the highWaterMark as shown above when creating the stream
+await files.addChunked(name, stream, null, true);
+
+

IFile.setStreamContentChunked

+
import { SPDefault } from "@pnp/nodejs";
+import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs/index.js";
+import "@pnp/sp/folders/web.js";
+import "@pnp/sp/folders/list.js";
+import "@pnp/sp/files/web.js";
+import "@pnp/sp/files/folder.js";
+import * as fs from "fs";
+
+const sp = spfi("https://something.com").using(SPDefault({
+    // config
+}));
+
+// NOTE: you must supply the highWaterMark to determine the block size for stream uploads
+const stream = fs.createReadStream("{file path}", { highWaterMark: 10485760 });
+const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName("file-name.txt");
+
+await file.setStreamContentChunked(stream);
+
+

Explicit import

+

If you don't need to import anything from the library, but would like to include the extensions just import the library as shown.

+
import "@pnp/nodejs";
+
+// get the stream
+const streamResult = await sp.web.getFileByServerRelativeUrl("/sites/dev/file.txt").getStream();
+
+

Accessing SP Extension Namespace

+

There are classes and interfaces included in extension modules, which you can access through a namespace, "SPNS".

+
import { SPNS } from "@pnp/nodejs-commonjs";
+
+const parser = new SPNS.StreamParser();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/packages/index.html b/packages/index.html new file mode 100644 index 000000000..7651a2c6a --- /dev/null +++ b/packages/index.html @@ -0,0 +1,2784 @@ + + + + + + + + + + + + + + + + + + + + + + + + Packages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

Packages

+

The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope.

+

The latest published version is npm version.

+

Core

+

Central to everything PnPjs builds on with utility methods, Timeline, the behavior plumbing, and the extendable framework.

+

npm install @pnp/core --save

+

Graph

+

This package provides a fluent SDK for calling the Microsoft Graph.

+

npm install @pnp/graph --save

+

Logging

+

A light-weight, subscribable logging framework.

+

npm install @pnp/logging --save

+

MSAL JS Client

+

Provides an msal wrapper suitable for use with PnPjs's request structure.

+

npm install @pnp/msaljsclient --save

+

Nodejs

+

Provides functionality enabling the @pnp libraries within nodejs, including extension methods for working with streams.

+

npm install @pnp/nodejs --save

+

Queryable

+

Extending Timeline this package provides the base functionality to create web requests in a fluent manner. It defines the available moments to which observers are subscribed for building the request.

+

npm install @pnp/queryable --save

+

SP

+

This package provides a fluent SDK for calling SharePoint.

+

npm install @pnp/sp --save

+

SP-Admin

+

This package provides a fluent SDK for calling SharePoint tenant admin APIs

+

npm install @pnp/sp-admin --save

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/queryable/behaviors/index.html b/queryable/behaviors/index.html new file mode 100644 index 000000000..0784b34e5 --- /dev/null +++ b/queryable/behaviors/index.html @@ -0,0 +1,3431 @@ + + + + + + + + + + + + + + + + + + + + + + + + behaviors - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/queryable : behaviors

+

The article describes the behaviors exported by the @pnp/queryable library. Please also see available behaviors in @pnp/core, @pnp/nodejs, @pnp/sp, and @pnp/graph.

+

Generally you won't need to use these behaviors individually when using the defaults supplied by the library, but when appropriate you can create your own composed behaviors using these as building blocks.

+

Bearer Token

+

Allows you to inject an existing bearer token into the request. This behavior will not replace any existing authentication behaviors, so you may want to ensure they are cleared if you are supplying your own tokens, regardless of their source. This behavior does no caching or performs any operation other than including your token in an authentication heading.

+
import { BearerToken } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BearerToken("HereIsMyBearerTokenStringFromSomeSource"));
+
+// optionally clear any configured authentication as you are supplying a token so additional calls shouldn't be needed
+// but take care as other behaviors may add observers to auth
+sp.on.auth.clear();
+
+// the bearer token supplied above will be applied to all requests made from `sp`
+const webInfo = await sp.webs();
+
+

BrowserFetch

+

This behavior, for use in web browsers, provides basic fetch support through the browser's fetch global method. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing

+
+

For fetch configuration in nodejs please see @pnp/nodejs behaviors.

+
+
import { BrowserFetch } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetch());
+
+const webInfo = await sp.webs();
+
+
import { BrowserFetch } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetch({ replace: false }));
+
+const webInfo = await sp.webs();
+
+

BrowserFetchWithRetry

+

This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.

+
import { BrowserFetchWithRetry } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetchWithRetry());
+
+const webInfo = await sp.webs();
+
+

You can also control how the behavior works through its props. The replace value works as described above for BrowserFetch. interval specifies the initial dynamic back off value in milliseconds. This value is ignored if a "Retry-After" header exists in the response. retries indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.

+
import { BrowserFetchWithRetry } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BrowserFetchWithRetry({
+    retries: 2,
+    interval: 400,
+    replace: true,
+}));
+
+const webInfo = await sp.webs();
+
+

Caching

+

This behavior allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map. It is a good idea to include caching in your projects to improve performance. By default items in the cache will expire after 5 minutes.

+
import { Caching } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(Caching());
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const webInfo = await sp.webs();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const webInfo2 = await sp.webs();
+
+

Custom Key Function

+

You can also supply custom functionality to control how keys are generated and calculate the expirations.

+

The cache key factory has the form (url: string) => string and you must ensure your keys are unique enough that you won't have collisions.

+

The expire date factory has the form (url: string) => Date and should return the Date when the cached data should expire. If you know that some particular data won't expire often you can set this date far in the future, or for more frequently updated information you can set it lower. If you set the expiration too short there is no reason to use caching as any stored information will likely always be expired. Additionally, you can set the storage to use local storage which will persist across sessions.

+
+

Note that for sp.search() requests if you want to specify a key you will need to use the CacheKey behavior below, the keyFactory value will be overwritten

+
+
import { getHashCode, PnPClientStorage, dateAdd, TimelinePipe } from "@pnp/core";
+import { Caching } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(Caching({
+    store: "local",
+    // use a hascode for the key
+    keyFactory: (url) => getHashCode(url.toLowerCase()).toString(),
+    // cache for one minute
+    expireFunc: (url) => dateAdd(new Date(), "minute", 1),
+}));
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const webInfo = await sp.webs();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const webInfo2 = await sp.webs();
+
+

As with any behavior you have the option to only apply caching to certain requests:

+
import { getHashCode, dateAdd } from "@pnp/core";
+import { Caching } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// caching will only apply to requests using `cachingList` as the base of the fluent chain
+const cachingList = sp.web.lists.getByTitle("{List Title}").using(Caching());
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const itemsInfo = await cachingList.items();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const itemsInfo2 = await cachingList.items();
+
+

bindCachingCore

+

Added in 3.10.0

+

The bindCachingCore method is supplied to allow all caching behaviors to share a common logic around the handling of ICachingProps. Usage of this function is not required to build your own caching method. However, it does provide consistent logic and will incoroporate any future enhancements. It can be used to create your own caching behavior. Here we show how we use the binding function within Caching as a basic example.

+

The bindCachingCore method is designed for use in a pre observer and the first two parameters are the url and init passed to pre. The third parameter is an optional Partial. It returns a tuple with three values. The first is a calculated value indicating if this request should be cached based on the internal default logic of the library, you can use this value in conjunction with your own logic. The second value is a function that will get a cached value, note no key is passed - the key is calculated and held within bindCachingCore. The third value is a function to which you pass a value to cache. The key and expiration are similarly calculated and held within bindCachingCore.

+
import { TimelinePipe } from "@pnp/core";
+import { bindCachingCore, ICachingProps, Queryable } from "@pnp/queryable";
+
+export function Caching(props?: ICachingProps): TimelinePipe<Queryable> {
+
+    return (instance: Queryable) => {
+
+        instance.on.pre(async function (this: Queryable, url: string, init: RequestInit, result: any): Promise<[string, RequestInit, any]> {
+
+            const [shouldCache, getCachedValue, setCachedValue] = bindCachingCore(url, init, props);
+
+            // only cache get requested data or where the CacheAlways header is present (allows caching of POST requests)
+            if (shouldCache) {
+
+                const cached = getCachedValue();
+
+                 // we need to ensure that result stays "undefined" unless we mean to set null as the result
+                if (cached === null) {
+
+                    // if we don't have a cached result we need to get it after the request is sent. Get the raw value (un-parsed) to store into cache
+                    this.on.rawData(noInherit(async function (response) {
+                        setCachedValue(response);
+                    }));
+
+                } else {
+                    // if we find it in cache, override send request, and continue flow through timeline and parsers.
+                    this.on.auth.clear();
+                    this.on.send.replace(async function (this: Queryable) {
+                        return new Response(cached, {});
+                    });
+                }
+            }
+
+            return [url, init, result];
+        });
+
+        return instance;
+    };
+}
+
+

CacheKey

+

Added in 3.5.0

+

This behavior allows you to set a pre-determined cache key for a given request. It needs to be used PER request otherwise the value will be continuously overwritten.

+
import { Caching, CacheKey } from "@pnp/queryable";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...).using(Caching());
+
+// note the application of the behavior on individual requests, if you share a CacheKey behavior across requests you'll encounter conflicts
+const webInfo = await sp.web.using(CacheKey("MyWebInfoCacheKey"))();
+
+const listsInfo = await sp.web.lists.using(CacheKey("MyListsInfoCacheKey"))();
+
+

CacheAlways

+

Added in 3.8.0

+

This behavior allows you to force caching for a given request. This should not be used for update/create operations as the request will not execute if a result is found in the cache

+
import { Caching, CacheAlways } from "@pnp/queryable";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...).using(Caching());
+
+const webInfo = await sp.web.using(CacheAlways())();
+
+

CacheNever

+

Added in 3.10.0

+

This behavior allows you to force skipping caching for a given request.

+
import { Caching, CacheNever } from "@pnp/queryable";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...).using(Caching());
+
+const webInfo = await sp.web.using(CacheNever())();
+
+

Caching Pessimistic Refresh

+

This behavior is slightly different than our default Caching behavior in that it will always return the cached value if there is one, but also asyncronously update the cached value in the background. Like the default CAchine behavior it allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map.

+

If you do not provide an expiration function then the cache will be updated asyncronously on every call, if you do provide an expiration then the cached value will only be updated, although still asyncronously, only when the cache has expired.

+
import { CachingPessimisticRefresh } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(CachingPessimisticRefresh());
+
+// caching will save the data into session storage on the first request - the key is based on the full url including query strings
+const webInfo = await sp.webs();
+
+// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)
+const webInfo2 = await sp.webs();
+
+

Again as with the default Caching behavior you can provide custom functions for key generation and expiration. Please see the Custom Key Function documentation above for more details.

+

InjectHeaders

+

Adds any specified headers to a given request. Can be used multiple times with a timeline. The supplied headers are added to all requests, and last applied wins - meaning if two InjectHeaders are included in the pipeline which inlcude a value for the same header, the second one applied will be used.

+
import { InjectHeaders } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(InjectHeaders({
+    "X-Something": "a value",
+    "MyCompanySpecialAuth": "special company token",
+}));
+
+const webInfo = await sp.webs();
+
+

Parsers

+

Parsers convert the returned fetch Response into something usable. We have included the most common parsers we think you'll need - but you can always write your own parser based on the signature of the parse moment.

+
+

All of these parsers when applied through using will replace any other observers on the parse moment.

+
+

DefaultParse

+

Performs error handling and parsing of JSON responses. This is the one you'll use for most of your requests and it is included in all the defaults.

+
import { DefaultParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(DefaultParse());
+
+const webInfo = await sp.webs();
+
+

TextParse

+

Checks for errors and parses the results as text with no further manipulation.

+
import { TextParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(TextParse());
+
+

BlobParse

+

Checks for errors and parses the results a Blob with no further manipulation.

+
import { BlobParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BlobParse());
+
+

JSONParse

+

Checks for errors and parses the results as JSON with no further manipulation. Meaning you will get the raw JSON response vs DefaultParse which will remove wrapping JSON.

+
import { JSONParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(JSONParse());
+
+

BufferParse

+

Checks for errors and parses the results a Buffer with no further manipulation.

+
import { BufferParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(BufferParse());
+
+

HeaderParse

+

Checks for errors and parses the headers of the Response as the result. This is a specialised parses which can be used in those infrequent scenarios where you need information from the headers of a response.

+
import { HeaderParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(HeaderParse());
+
+

JSONHeaderParse

+

Checks for errors and parses the headers of the Respnose as well as the JSON and returns an object with both values.

+
import { JSONHeaderParse } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(JSONHeaderParse());
+
+...sp.data
+...sp.headers
+
+

Resolvers

+

These two behaviors are special and should always be included when composing your own defaults. They implement the expected behavior of resolving or rejecting the promise returned when executing a timeline. They are implemented as behaviors should there be a need to do something different the logic is not locked into the core of the library.

+

ResolveOnData, RejectOnError

+
import { ResolveOnData, RejectOnError } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const sp = spfi(...).using(ResolveOnData(), RejectOnError());
+
+

Timeout

+

The Timeout behavior allows you to include a timeout in requests. You can specify either a number, representing the number of milliseconds until the request should timeout or an AbortSignal.

+
+

In Nodejs you will need to polyfill AbortController if your version (<15) does not include it when using Timeout and passing a number. If you are supplying your own AbortSignal you do not.

+
+
import { Timeout } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+// requests should timeout in 5 seconds
+const sp = spfi(...).using(Timeout(5000));
+
+
import { Timeout } from "@pnp/queryable";
+
+import "@pnp/sp/webs";
+
+const controller = new AbortController();
+
+const sp = spfi(...).using(Timeout(controller.signal));
+
+// abort requests after 6 seconds using our own controller
+const timer = setTimeout(() => {
+    controller.abort();
+}, 6000);
+
+// this request will be cancelled if it doesn't complete in 6 seconds
+const webInfo = await sp.webs();
+
+// be a good citizen and cancel unneeded timers
+clearTimeout(timer);
+
+

Cancelable

+

Beta

+

Updated as Beta 2 in 3.5.0

+

This behavior allows you to cancel requests before they are complete. It is similar to timeout however you control when and if the request is canceled. Please consider this behavior as beta while we work to stabalize the functionality.

+

Known Issues

+
    +
  • Due to how the event loop works you may get unhandled rejections after canceling a request
  • +
+
import { Cancelable, CancelablePromise } from "@pnp/queryable";
+import { IWebInfo } from "@pnp/sp/webs";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(Cancelable());
+
+const p: CancelablePromise<IWebInfo> = <any>sp.web();
+
+setTimeout(() => {
+
+    // you should await the cancel operation to ensure it completes
+    await p.cancel();
+}, 200);
+
+// this is awaiting the results of the request
+const webInfo: IWebInfo = await p;
+
+

Cancel long running operations

+

Some operations such as chunked uploads that take longer to complete are good candidates for canceling based on user input such as a button select.

+
import { Cancelable, CancelablePromise } from "@pnp/queryable";
+import { IFileAddResult } from "@pnp/sp/files";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import { getRandomString } from "@pnp/core";
+import { createReadStream } from "fs";
+
+const sp = spfi().using(Cancelable());
+
+const file = createReadStream(join("C:/some/path", "test.mp4"));
+
+const p: CancelablePromise<IFileAddResult> = <any>sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents").files.addChunked(`te's't-${getRandomString(4)}.mp4`, <any>file);
+
+setTimeout(() => {
+
+    // you should await the cancel operation to ensure it completes
+    await p.cancel();
+}, 10000);
+
+// this is awaiting the results of the request
+await p;
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/queryable/extensions/index.html b/queryable/extensions/index.html new file mode 100644 index 000000000..162a68a43 --- /dev/null +++ b/queryable/extensions/index.html @@ -0,0 +1,2957 @@ + + + + + + + + + + + + + + + + + + + + + + + + extensions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

Extensions

+

Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invokable and allow you to control any behavior of the library with extensions.

+

Types of Extensions

+

There are two types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.

+

Function Extensions

+

The first type is a simple function with a signature:

+
(op: "apply" | "get" | "has" | "set", target: T, ...rest: any[]): void
+
+

This function is passed the current operation as the first argument, currently one of "apply", "get", "has", or "set". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.

+

Named Extensions

+

Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables.

+
import { extendFactory } from "@pnp/queryable";
+import { sp, List, Lists, IWeb, ILists, List, IList, Web } from "@pnp/sp/presets/all";
+import { escapeQueryStrValue } from "@pnp/sp/utils/escapeQueryStrValue";
+
+// create a plain object with the props and methods we want to add/change
+const myExtensions = {
+    // override the lists property
+    get lists(this: IWeb): ILists {
+        // we will always order our lists by title and select just the Title for ALL calls (just as an example)
+        return Lists(this).orderBy("Title").select("Title");
+    },
+    // override the getByTitle method
+    getByTitle: function (this: ILists, title: string): IList {
+        // in our example our list has moved, so we rewrite the request on the fly
+        if (title === "List1") {
+            return List(this, `getByTitle('List2')`);
+        } else {
+            // you can't at this point call the "base" method as you will end up in loop within the proxy
+            // so you need to ensure you patch/include any original functionality you need
+            return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
+        }
+    },
+};
+
+// register all the named Extensions
+extendFactory(Web, myExtensions);
+
+// this will use our extension to ensure the lists are ordered
+const lists = await sp.web.lists();
+
+console.log(JSON.stringify(lists, null, 2));
+
+// we will get the items from List1 but within the extension it is rewritten as List2
+const items = await sp.web.lists.getByTitle("List1").items();
+
+console.log(JSON.stringify(items.length, null, 2));
+
+

ProxyHandler Extensions

+

You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work.

+
import { extendFactory } from "@pnp/queryable";
+import { sp, Lists, IWeb, ILists, Web } from "@pnp/sp/presets/all";
+import { escapeQueryStrValue } from "@pnp/sp/utils/escapeSingleQuote";
+
+const myExtensions = {
+    get: (target, p: string | number | symbol, _receiver: any) => {
+        switch (p) {
+            case "getByTitle":
+                return (title: string) => {
+
+                    // in our example our list has moved, so we rewrite the request on the fly
+                    if (title === "LookupList") {
+                        return List(target, `getByTitle('OrderByList')`);
+                    } else {
+                        // you can't at this point call the "base" method as you will end up in loop within the proxy
+                        // so you need to ensure you patch/include any original functionality you need
+                        return List(target, `getByTitle('${escapeQueryStrValue(title)}')`);
+                    }
+                };
+        }
+    },
+};
+
+extendFactory(Web, myExtensions);
+
+const lists = sp.web.lists;
+const items = await lists.getByTitle("LookupList").items();
+
+console.log(JSON.stringify(items.length, null, 2));
+
+

Registering Extensions

+

You can register Extensions on an invocable factory or on a per-object basis, and you can register a single extension or an array of Extensions.

+

Factory Registration

+

The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { IWeb, Web } from "@pnp/sp/webs";
+import { ILists, Lists } from "@pnp/sp/lists";
+import { extendFactory } from "@pnp/queryable";
+import { sp } from "@pnp/sp";
+
+const sp = spfi().using(...);
+
+// sets up the types correctly when importing across your application
+declare module "@pnp/sp/webs/types" {
+
+    // we need to extend the interface
+    interface IWeb {
+        orderedLists: ILists;
+    }
+}
+
+// sets up the types correctly when importing across your application
+declare module "@pnp/sp/lists/types" {
+
+    // we need to extend the interface
+    interface ILists {
+        getOrderedListsQuery: (this: ILists) => ILists;
+    }
+}
+
+extendFactory(Web, {
+    // add an ordered lists property
+    get orderedLists(this: IWeb): ILists {
+        return this.lists.getOrderedListsQuery();
+    },
+});
+
+extendFactory(Lists, {
+    // add an ordered lists property
+    getOrderedListsQuery(this: ILists): ILists {
+        return this.top(10).orderBy("Title").select("Title");
+    },
+});
+
+// regardless of how we access the web and lists collections our extensions remain with all new instance based on
+const web = Web([sp.web, "https://tenant.sharepoint.com/sites/dev/"]);
+const lists1 = await web.orderedLists();
+console.log(JSON.stringify(lists1, null, 2));
+
+const lists2 = await Web([sp.web, "https://tenant.sharepoint.com/sites/dev/"]).orderedLists();
+console.log(JSON.stringify(lists2, null, 2));
+
+const lists3 = await sp.web.orderedLists();
+console.log(JSON.stringify(lists3, null, 2));
+
+

Instance Registration

+

You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances.

+
+

Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are.

+
+

Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance.

+
import { extendObj } from "@pnp/queryable";
+import { sp, List, ILists } from "@pnp/sp/presets/all";
+
+const myExtensions = {
+    getByTitle: function (this: ILists, title: string) {
+        // in our example our list has moved, so we rewrite the request on the fly
+        if (title === "List1") {
+            return List(this, "getByTitle('List2')");
+        } else {
+            // you can't at this point call the "base" method as you will end up in loop within the proxy
+            // so you need to ensure you patch/include any original functionality you need
+            return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
+        }
+    },
+};
+
+const lists =  extendObj(sp.web.lists, myExtensions);
+const items = await lists.getByTitle("LookupList").items();
+
+console.log(JSON.stringify(items.length, null, 2));
+
+

Enable & Disable Extensions and Clear Global Extensions

+

Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed.

+
import { enableExtensions, disableExtensions, clearGlobalExtensions } from "@pnp/queryable";
+
+// disable Extensions
+disableExtensions();
+
+// enable Extensions
+enableExtensions();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/queryable/queryable/index.html b/queryable/queryable/index.html new file mode 100644 index 000000000..23fede94a --- /dev/null +++ b/queryable/queryable/index.html @@ -0,0 +1,3005 @@ + + + + + + + + + + + + + + + + + + + + + + + + queryable - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/queryable/queryable

+

Queryable is the base class for both the sp and graph fluent interfaces and provides the structure to which observers are registered. As a background to understand more of the mechanics please see the articles on Timeline, moments, and observers. For reuse it is recommended to compose your observer registrations with behaviors.

+

Queryable Constructor

+

By design the library is meant to allow creating the next part of a url from the current part. In this way each queryable instance is built from a previous instance. As such understanding the Queryable constructor's behavior is important. The constructor takes two parameters, the first required and the second optional.

+

The first parameter can be another queryable, a string, or a tuple of [Queryable, string].

+ + + + + + + + + + + + + + + + + + + + + +
ParameterBehavior
QueryableThe new queryable inherits all of the supplied queryable's observers. Any supplied path (second constructor param) is appended to the supplied queryable's url becoming the url of the newly constructed queryable
stringThe new queryable will have NO registered observers. Any supplied path (second constructor param) is appended to the string becoming the url of the newly constructed queryable
[Queryable, string]The observers from the supplied queryable are used by the new queryable. The url is a combination of the second tuple argument (absolute url string) and any supplied path.
+
+

The tuple constructor call can be used to rebase a queryable to call a different host in an otherwise identical way to another queryable. When using the tuple constructor the url provided must be absolute.

+
+

Examples

+
// represents a fully configured queryable with url and registered observers
+// url: https://something.com
+const baseQueryable;
+
+// child1 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://something.com/subpath"
+const child1 = Child(baseQueryable, "subpath");
+
+// child2 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://something.com"
+const child2 = Child(baseQueryable);
+
+// nonchild1 will:
+// - have NO registered observers or connection to baseQueryable
+// - have a url of "https://somethingelse.com"
+const nonchild1 = Child("https://somethingelse.com");
+
+// nonchild2 will:
+// - have NO registered observers or connection to baseQueryable
+// - have a url of "https://somethingelse.com/subpath"
+const nonchild2 = Child("https://somethingelse.com", "subpath");
+
+// rebased1 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://somethingelse.com"
+const rebased1 = Child([baseQueryable, "https://somethingelse.com"]);
+
+// rebased2 will:
+// - reference the observers of baseQueryable
+// - have a url of "https://somethingelse.com/subpath"
+const rebased2 = Child([baseQueryable, "https://somethingelse.com"], "subpath");
+
+

Queryable Lifecycle

+

The Queryable lifecycle is:

+
    +
  • construct (Added in 3.5.0)
  • +
  • init
  • +
  • pre
  • +
  • auth
  • +
  • send
  • +
  • parse
  • +
  • post
  • +
  • data
  • +
  • dispose
  • +
+

As well log and error can emit at any point during the lifecycle.

+

No observers registered for this request

+

If you see an error thrown with the message No observers registered for this request. it means at the time of execution the given object has no actions to take. Because all the request logic is defined within observers, an absence of observers is likely an error condition. If the object was created by a method within the library please report an issue as it is likely a bug. If you created the object through direct use of one of the factory functions, please be sure you have registered observers with using or on as appropriate. More information on observers is available in this article.

+

If you for some reason want to execute a queryable with no registred observers, you can simply register a noop observer to any of the moments.

+

Queryable Observers

+

This section outlines how to write observers for the Queryable lifecycle, and the expectations of each moment's observer behaviors.

+
+

In the below samples consider the variable query to mean any valid Queryable derived object.

+
+

log

+

Anything can log to a given timeline's log using the public log method and to intercept those message you can subscribed to the log event.

+

The log observer's signature is: (this: Timeline<T>, message: string, level: number) => void

+
query.on.log((message, level) => {
+
+    // log only warnings or errors
+    if (level > 1) {
+        console.log(message);
+    }
+});
+
+
+

The level value is a number indicating the severity of the message. Internally we use the values from the LogLevel enum in @pnp/logging: Verbose = 0, Info = 1, Warning = 2, Error = 3. Be aware that nothing enforces those values other than convention and log can be called with any value for level.

+
+

As well we provide easy support to use PnP logging within a Timeline derived class:

+
import { LogLevel, PnPLogging } from "@pnp/logging";
+
+// any messages of LogLevel Info or higher (1) will be logged to all subscribers of the logging framework
+query.using(PnPLogging(LogLevel.Info));
+
+
+

More details on the pnp logging framework

+
+

error

+

Errors can happen at anytime and for any reason. If you are using the RejectOnError behavior, and both sp and graph include that in the defaults, the request promise will be rejected as expected and you can handle the error that way.

+

The error observer's signature is: (this: Timeline<T>, err: string | Error) => void

+
import { spfi, DefaultInit, DefaultHeaders } from "@pnp/sp";
+import { BrowserFetchWithRetry, DefaultParse } from "@pnp/queryable";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(DefaultInit(), DefaultHeaders(), BrowserFetchWithRetry(), DefaultParse());
+
+try {
+
+    const result = await sp.web();
+
+} catch(e) {
+
+    // any errors emitted will result in the promise being rejected
+    // and ending up in the catch block as expected
+}
+
+

In addition to the default behavior you can register your own observers on error, though it is recommended you leave the default behavior in place.

+
query.on.error((err) => {
+
+    if (err) {
+        console.error(err);
+        // do other stuff with the error (send it to telemetry)
+    }
+});
+
+

construct

+

Added in 3.5.0

+

This moment exists to assist behaviors that need to transfer some information from a parent to a child through the fluent chain. We added this to support cancelable scopes for the Cancelable behavior, but it may have other uses. It is invoked AFTER the new instance is fully realized via new and supplied with the parameters used to create the new instance. As with all moments the "this" within the observer is the current (NEW) instance.

+

For your observers on the construct method to work correctly they must be registered before the instance is created.

+
+

The construct moment is NOT async and is designed to support simple operations.

+
+
query.on.construct(function (this: Queryable, init: QueryableInit, path?: string): void {
+    if (typeof init !== "string") {
+
+        // get a ref to the parent Queryable instance used to create this new instance
+        const parent = isArray(init) ? init[0] : init;
+
+        if (Reflect.has(parent, "SomeSpecialValueKey")) {
+
+            // copy that specail value to the new child
+            this["SomeSpecialValueKey"] = parent["SomeSpecialValueKey"];
+        }
+    }     
+});
+
+query.on.pre(async function(url, init, result) {
+
+    // we have access to the copied special value throughout the lifecycle
+    this.log(this["SomeSpecialValueKey"]);
+
+    return [url, init, result];
+});
+
+query.on.dispose(() => {
+
+    // be a good citizen and clean up your behavior's values when you're done
+    delete this["SomeSpecialValueKey"];
+});
+
+

init

+

Along with dispose, init is a special moment that occurs before any of the other lifecycle providing a first chance at doing any tasks before the rest of the lifecycle starts. It is not await aware so only sync operations are supported in init by design.

+

The init observer's signature is: (this: Timeline<T>) => void

+
+

In the case of init you manipulate the Timeline instance itself

+
+
query.on.init(function (this: Queryable) {
+
+    // init is a great place to register additioanl observers ahead of the lifecycle
+    this.on.pre(async function (this: Quyerable, url, init, result) {
+        // stuff happens
+        return [url, init, result];
+    });
+});
+
+

pre

+

Pre is used by observers to configure the request before sending. Note there is a dedicated auth moment which is prefered by convention to handle auth related tasks.

+

The pre observer's signature is: (this: IQueryable, url: string, init: RequestInit, result: any) => Promise<[string, RequestInit, any]>

+
+

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

+
+

Example of when to use pre are updates to the init, caching scenarios, or manipulation of the url (ensuring it is absolute). The init passed to pre (and auth) is the same object that will be eventually passed to fetch, meaning you can add any properties/congifuration you need. The result should always be left undefined unless you intend to end the lifecycle. If pre completes and result has any value other than undefined that value will be emitted to data and the timeline lifecycle will end.

+
query.on.pre(async function(url, init, result) {
+
+    init.cache = "no-store";
+
+    return [url, init, result];
+});
+
+query.on.pre(async function(url, init, result) {
+
+    // setting result causes no moments after pre to be emitted other than data
+    // once data is emitted (resolving the request promise by default) the lifecycle ends
+    result = "My result";
+
+    return [url, init, result];
+});
+
+

auth

+

Auth functions very much like pre except it does not have the option to set the result, and the url is considered immutable by convention. Url manipulation should be done in pre. Having a seperate moment for auth allows for easily changing auth specific behavior without having to so a lot of complicated parsing of pre observers.

+

The auth observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<[URL, RequestInit]>.

+
+

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

+
+
query.on.auth(async function(url, init) {
+
+    // some code to get a token
+    const token = getToken();
+
+    init.headers["Authorization"] = `Bearer ${token}`;
+
+    return [url, init];
+});
+
+

send

+

Send is implemented using the request moment which uses the first registered observer and invokes it expecting an async Response.

+

The send observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<Response>.

+
query.on.send(async function(url, init) {
+
+    // this could represent reading a file, querying a database, or making a web call
+    return fetch(url.toString(), init);
+});
+
+

parse

+

Parse is responsible for turning the raw Response into something usable. By default we handle errors and parse JSON responses, but any logic could be injected here. Perhaps your company encrypts things and you need to decrypt them before parsing further.

+

The parse observer's signature is: (this: IQueryable, url: URL, response: Response, result: any | undefined) => Promise<[URL, Response, any]>.

+
+

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

+
+
// you should be careful running multiple parse observers so we replace with our functionality
+// remember every registered observer is run, so if you set result and a later observer sets a
+// different value last in wins.
+query.on.parse.replace(async function(url, response, result) {
+
+    if (response.ok) {
+
+        result = await response.json();
+
+    } else {
+
+        // just an example
+        throw Error(response.statusText);
+    }
+
+    return [url, response, result];
+});
+
+

post

+

Post is run after parse, meaning you should have a valid fully parsed result, and provides a final opportunity to do caching, some final checks, or whatever you might need immediately prior to the request promise resolving with the value. It is recommened to NOT manipulate the result within post though nothing prevents you from doing so.

+

The post observer's signature is: (this: IQueryable, url: URL, result: any | undefined) => Promise<[URL, any]>.

+
+

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

+
+
query.on.post(async function(url, result) {
+
+    // here we do some caching of a result
+    const key = hash(url);
+    cache(key, result);   
+
+    return [url, result];
+});
+
+

data

+

Data is called with the result of the Queryable lifecycle produced by send, understood by parse, and passed through post. By default the request promise will resolve with the value, but you can add any additional observers you need.

+

The data observer's signature is: (this: IQueryable, result: T) => void.

+
+

Clearing the data moment (ie. .on.data.clear()) after the lifecycle has started will result in the request promise never resolving

+
+
query.on.data(function(result) {
+
+    console.log(`Our result! ${JSON.stringify(result)}`);
+});
+
+

dispose

+

Along with init, dispose is a special moment that occurs after all other lifecycle moments have completed. It is not await aware so only sync operations are supported in dispose by design.

+

The dispose observer's signature is: (this: Timeline<T>) => void

+
+

In the case of dispose you manipulate the Timeline instance itself

+
+
query.on.dispose(function (this: Queryable) {
+
+    // maybe your queryable calls a database?
+    db.connection.close();
+});
+
+

Other Methods

+

Queryable exposes some additional methods beyond the observer registration.

+

concat

+

Appends the supplied string to the url without mormalizing slashes.

+
// url: something.com/items
+query.concat("(ID)");
+// url: something.com/items(ID)
+
+

toRequestUrl

+

Converts the queryable's internal url parameters (url and query) into a relative or absolute url.

+
const s = query.toRequestUrl();
+
+

query

+

Map used to manage any query string parameters that will be included. Anything added here will be represented in toRequestUrl's output.

+
query.query.add("$select", "Title");
+
+

toUrl

+

Returns the url currently represented by the Queryable, without the querystring part

+
const s = query.toUrl();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/search/search_index.json b/search/search_index.json new file mode 100644 index 000000000..ffabd698b --- /dev/null +++ b/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":"

PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.

These articles provide general guidance for working with the libraries. If you are migrating from V2 please review the transition guide.

  • Getting Started
  • Authentication
  • Get Started Contributing

Animation of the library in use, note intellisense help in building your queries

"},{"location":"#packages","title":"Packages","text":"

Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.

The latest published version is .

@pnp/ azidjsclient Provides an Azure Identity wrapper suitable for use with PnPjs core Provides shared functionality across all pnp libraries graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs queryable Provides shared query functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-admin Provides a fluent api for working with M365 Tenant admin methods"},{"location":"#authentication","title":"Authentication","text":"

We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!

"},{"location":"#issues-questions-ideas","title":"Issues, Questions, Ideas","text":"

Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.

"},{"location":"#changelog","title":"Changelog","text":"

Please review the CHANGELOG for release details on all library changes.

"},{"location":"#code-of-conduct","title":"Code of Conduct","text":"

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

"},{"location":"#sharing-is-caring","title":"\"Sharing is Caring\"","text":"

Please use http://aka.ms/community/home for the latest updates around the whole Microsoft 365 and Power Platform Community(PnP) initiative.

"},{"location":"#disclaimer","title":"Disclaimer","text":"

THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

"},{"location":"getting-started/","title":"Getting Started","text":"

This library is geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.

If you need to support older browsers, SharePoint on-premisis servers, or older versions of the SharePoint Framework, please revert to version 2 of the library and see related documentation on polyfills for required functionality.

"},{"location":"getting-started/#minimal-requirements","title":"Minimal Requirements","text":"
- NodeJs: >= 14\n- TypeScript: 4.x\n- Node Modules Supported: ESM Only\n
"},{"location":"getting-started/#install","title":"Install","text":"

First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. @pnp/sp to access the SharePoint REST API and @pnp/graph to access some of the Microsoft Graph API. This step applies to any environment or project.

npm install @pnp/sp @pnp/graph --save

Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples.

import { getRandomString } from \"@pnp/core\";\n\n(function() {\n\n    // get and log a random string\n    console.log(getRandomString(20));\n\n})()\n
"},{"location":"getting-started/#getting-started-with-sharepoint-framework","title":"Getting Started with SharePoint Framework","text":"

The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises you will need to use version 2 of the library. If you are targeting SharePoint online you will need to take the additional steps outlined below based on the version of the SharePoint Framework you are targeting.

We've created two Getting Started samples. The first uses the more traditional React Component classes and can be found in the react-pnp-js-sample project, utilizing SPFx 1.15.2 and PnPjs V3, it showcases some of the more dramatic changes to the library. There is also a companion video series on YouTube if you prefer to see things done through that medium here's a link to the playlist for the 5 part series:

Getting started with PnPjs 3.0: 5-part series

In addition, we have converted the sample project from React Component to React Hooks. This version can be found in react-pnp-js-hooks. This sample will help those struggling to establish context correctly while using the hooks conventions.

The SharePoint Framework supports different versions of TypeScript natively and as of 1.14 release still doesn't natively support TypeScript 4.x. Sadly, this means that to use Version 3 of PnPjs you will need to take a few additional configuration steps to get them to work together.

"},{"location":"getting-started/#spfx-version-1150-later","title":"SPFx Version 1.15.0 & later","text":"

No additional steps required

"},{"location":"getting-started/#spfx-version-1121-1140","title":"SPFx Version 1.12.1 => 1.14.0","text":"
  1. Update the rush stack compiler to 4.2. This is covered in this great article by Elio, but the steps are listed below.

    • Uninstall existing rush stack compiler (replace the ? with the version that is currently referenced in your package.json): npm uninstall @microsoft/rush-stack-compiler-3.?
    • Install 4.2 version: npm i @microsoft/rush-stack-compiler-4.2
    • Update tsconfig.json to extend the 4.2 config: \"extends\": \"./node_modules/@microsoft/rush-stack-compiler-4.2/includes/tsconfig-web.json\"
  2. Replace the contents of the gulpfile.js with: >Note: The only change is the addition of the line to disable tslint.

    ```js 'use strict';

    const build = require('@microsoft/sp-build-web');

    build.addSuppression(Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.);

    var getTasks = build.rig.getTasks; build.rig.getTasks = function () { var result = getTasks.call(build.rig);

    result.set('serve', result.get('serve-deprecated'));\n\nreturn result;\n

    };

    // * ADDED * // disable tslint build.tslintCmd.enabled = false; // * ADDED *

    build.initialize(require('gulp')); ```

"},{"location":"getting-started/#spfx-version-1110-earlier","title":"SPFx Version 1.11.0 & earlier","text":"

At this time there is no documented method to use version 3.x with SPFx versions earlier than 1.12.1. We recommend that you fall back to using version 2 of the library or update your SPFx version.

"},{"location":"getting-started/#imports-and-usage","title":"Imports and usage","text":"

Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. To establish context within the library you will need to use the SharePoint or Graph Factory Interface depending on which set of APIs you want to utilize. For SharePoint you will use the spfi interface and for the Microsoft Graph you will use the graphfi interface whic are both in the main export of the corresponding package. Examples of both methods are shown below.

Depending on how you architect your solution establishing context is done where you want to make calls to the API. The examples demonstrate doing so in the onInit method as a local variable but this could also be done to a private variable or passed into a service.

Note if you are going to use both the @pnp/sp and @pnp/graph packages in SPFx you will need to alias the SPFx behavior import, please see the section below for more details.

"},{"location":"getting-started/#using-pnpsp-spfi-factory-interface-in-spfx","title":"Using @pnp/sp spfi factory interface in SPFx","text":"
import { spfi, SPFx } from \"@pnp/sp\";\n\n// ...\n\nprotected async onInit(): Promise<void> {\n\n    await super.onInit();\n    const sp = spfi().using(SPFx(this.context));\n\n}\n\n// ...\n\n
"},{"location":"getting-started/#using-pnpgraph-graphfi-factory-interface-in-spfx","title":"Using @pnp/graph graphfi factory interface in SPFx","text":"
import { graphfi, SPFx } from \"@pnp/graph\";\n\n// ...\n\nprotected async onInit(): Promise<void> {\n\n    await super.onInit();\n    const graph = graphfi().using(SPFx(this.context));\n\n}\n\n// ...\n\n
"},{"location":"getting-started/#using-both-pnpsp-and-pnpgraph-in-spfx","title":"Using both @pnp/sp and @pnp/graph in SPFx","text":"
\nimport { spfi, SPFx as spSPFx } from \"@pnp/sp\";\nimport { graphfi, SPFx as graphSPFx} from \"@pnp/graph\";\n\n// ...\n\nprotected async onInit(): Promise<void> {\n\n    await super.onInit();\n    const sp = spfi().using(spSPFx(this.context));\n    const graph = graphfi().using(graphSPFx(this.context));\n\n}\n\n// ...\n\n
"},{"location":"getting-started/#project-configservices-setup","title":"Project Config/Services Setup","text":"

Please see the documentation on setting up a config file or a services for more information about establishing and instance of the spfi or graphfi interfaces that can be reused. It is a common mistake with users of V3 that they try and create the interface in event handlers which causes issues.

"},{"location":"getting-started/#getting-started-with-nodejs","title":"Getting started with NodeJS","text":"

Due to the way in which Node resolves ESM modules when you use selective imports in node you must include the index.js part of the path. Meaning an import like import \"@pnp/sp/webs\" in examples must be import \"@pnp/sp/webs/index.js\". Root level imports such as import { spfi } from \"@pnp/sp\" remain correct. The samples in this section demonstrate this for their selective imports.

"},{"location":"getting-started/#importing-nodejs-support","title":"Importing NodeJS support","text":"

Note that the NodeJS integration relies on code in the module @pnp/nodejs. It is therefore required that you import this near the beginning of your program, using simply

js import \"@pnp/nodejs\";

"},{"location":"getting-started/#authentication","title":"Authentication","text":"

To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below.

mkdir \\temp\ncd \\temp\nopenssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'\nopenssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass\n

Using the above code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration.

"},{"location":"getting-started/#using-pnpsp-spfi-factory-interface-in-nodejs","title":"Using @pnp/sp spfi factory interface in NodeJS","text":"

Version 3 of this library only supports ESModules. If you still require commonjs modules please check out version 2.

The first step is to install the packages that will be needed. You can read more about what each package does starting on the packages page.

npm i @pnp/sp @pnp/nodejs\n

Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:

\nimport { SPDefault } from \"@pnp/nodejs\";\nimport \"@pnp/sp/webs/index.js\";\nimport { readFileSync } from 'fs';\nimport { Configuration } from \"@azure/msal-node\";\n\nfunction() {\n    // configure your node options (only once in your application)\n    const buffer = readFileSync(\"c:/temp/key.pem\");\n\n    const config: Configuration = {\n        auth: {\n            authority: \"https://login.microsoftonline.com/{tenant id or common}/\",\n            clientId: \"{application (client) i}\",\n            clientCertificate: {\n              thumbprint: \"{certificate thumbprint, displayed in AAD}\",\n              privateKey: buffer.toString(),\n            },\n        },\n    };\n\n    const sp = spfi().using(SPDefault({\n        baseUrl: 'https://{my tenant}.sharepoint.com/sites/dev/',\n        msal: {\n            config: config,\n            scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]\n        }\n    }));\n\n    // make a call to SharePoint and log it in the console\n    const w = await sp.web.select(\"Title\", \"Description\")();\n    console.log(JSON.stringify(w, null, 4));\n}();\n
"},{"location":"getting-started/#using-pnpgraph-graphfi-factory-interface-in-nodejs","title":"Using @pnp/graph graphfi factory interface in NodeJS","text":"

Similar to the above you can also make calls to the Microsoft Graph API from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example.

npm i @pnp/graph @pnp/nodejs\n

Now we need to import what we'll need to call graph

import { graphfi } from \"@pnp/graph\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users/index.js\";\n\nfunction() {\n    const graph = graphfi().using(GraphDefault({\n    baseUrl: 'https://graph.microsoft.com',\n    msal: {\n        config: config,\n        scopes: [ 'https://graph.microsoft.com/.default' ]\n    }\n    }));\n    // make a call to Graph and get all the groups\n    const userInfo = await graph.users.top(1)();\n    console.log(JSON.stringify(userInfo, null, 4));\n}();\n
"},{"location":"getting-started/#node-project-using-typescript-producing-commonjs-modules","title":"Node project using TypeScript producing commonjs modules","text":"

For TypeScript projects which output commonjs but need to import esm modules you will need to take a few additional steps to use the pnp esm modules. This is true of any esm module with a project structured in this way, not specific to PnP's implementation. It is very possible there are other configurations that make this work, but these steps worked in our testing. We have also provided a basic sample showing this setup.

You must install TypeScript @next or you will get errors using node12 module resolution. This may change but is the current behavior when we did our testing.

npm install -D typescript@next

The tsconfig file for your project should have the \"module\": \"CommonJS\" and \"moduleResolution\": \"node12\", settings in addition to whatever else you need.

tsconfig.json

{\n    \"compilerOptions\": {\n        \"module\": \"CommonJS\",\n        \"moduleResolution\": \"node12\"\n}\n

You must then import the esm dependencies using the async import pattern. This works as expected with our selective imports, and vscode will pick up the intellisense as expected.

index.ts

import { settings } from \"./settings.js\";\n\n// this is a simple example as async await is not supported with commonjs output\n// at the root.\nsetTimeout(async () => {\n\n    const { spfi } = await import(\"@pnp/sp\");\n    const { SPDefault } = await import(\"@pnp/nodejs\");\n    await import(\"@pnp/sp/webs/index.js\");\n\n    const sp = spfi().using(SPDefault({\n        baseUrl: settings.testing.sp.url,\n        msal: {\n            config: settings.testing.sp.msal.init,\n            scopes: settings.testing.sp.msal.scopes\n        }\n    }));\n\n    // make a call to SharePoint and log it in the console\n    const w = await sp.web.select(\"Title\", \"Description\")();\n    console.log(JSON.stringify(w, null, 4));\n\n}, 0);\n

Finally, when launching node you need to include the `` flag with a setting of 'node'.

node --experimental-specifier-resolution=node dist/index.js

Read more in the releated TypeScript Issue, TS pull request Adding the functionality, and the TS Docs.

"},{"location":"getting-started/#single-page-application-context","title":"Single Page Application Context","text":"

In some cases you may be working in a client-side application that doesn't have context to the SharePoint site. In that case you will need to utilize the MSAL Client, you can get the details on creating that connection in this article.

"},{"location":"getting-started/#selective-imports","title":"Selective Imports","text":"

This library has a lot of functionality and you may not need all of it. For that reason, we support selective imports which allow you to only import the parts of the sp or graph library you need, which reduces your overall solution bundle size - and enables treeshaking.

You can read more about selective imports.

"},{"location":"getting-started/#error-handling","title":"Error Handling","text":"

This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.

"},{"location":"getting-started/#extending-the-library","title":"Extending the Library","text":"

Because of the way the fluent library is designed by definition it's extendible. That means that if you want to build your own custom functions that extend the features of the library this can be done fairly simply. To get more information about creating your own custom extensions check out extending the library article.

"},{"location":"getting-started/#connect-to-a-different-web","title":"Connect to a different Web","text":"

The new factory function allows you to create a connection to a different web maintaining the same setup as your existing interface. You have two options, either to 'AssignFrom' or 'CopyFrom' the base timeline's observers. The below example utilizes 'AssignFrom' but the method would be the same regadless of which route you choose. For more information on these behaviors see Core/Behaviors.

import { spfi, SPFx } from \"@pnp/sp\";\nimport { AssignFrom } from \"@pnp/core\";\nimport \"@pnp/sp/webs\";\n\n//Connection to the current context's Web\nconst sp = spfi(...);\n\n// Option 1: Create a new instance of Queryable\nconst spWebB = spfi({Other Web URL}).using(SPFx(this.context));\n\n// Option 2: Copy/Assign a new instance of Queryable using the existing\nconst spWebB = spfi({Other Web URL}).using(AssignFrom(sp.web));\n\n// Option 3: Create a new instance of Queryable using other credentials?\nconst spWebB = spfi({Other Web URL}).using(SPFx(this.context));\n\n// Option 4: Create new Web instance by using copying SPQuerable and new pointing to new web url (e.g. https://contoso.sharepoint.com/sites/Web2)\nconst web = Web([sp.web, {Other Web URL}]);\n
"},{"location":"getting-started/#next-steps","title":"Next Steps","text":"

For more complicated authentication scnearios please review the article describing all of the available authentication methods.

"},{"location":"packages/","title":"Packages","text":"

The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope.

The latest published version is .

"},{"location":"packages/#core","title":"Core","text":"

Central to everything PnPjs builds on with utility methods, Timeline, the behavior plumbing, and the extendable framework.

npm install @pnp/core --save

"},{"location":"packages/#graph","title":"Graph","text":"

This package provides a fluent SDK for calling the Microsoft Graph.

npm install @pnp/graph --save

"},{"location":"packages/#logging","title":"Logging","text":"

A light-weight, subscribable logging framework.

npm install @pnp/logging --save

"},{"location":"packages/#msal-js-client","title":"MSAL JS Client","text":"

Provides an msal wrapper suitable for use with PnPjs's request structure.

npm install @pnp/msaljsclient --save

"},{"location":"packages/#nodejs","title":"Nodejs","text":"

Provides functionality enabling the @pnp libraries within nodejs, including extension methods for working with streams.

npm install @pnp/nodejs --save

"},{"location":"packages/#queryable","title":"Queryable","text":"

Extending Timeline this package provides the base functionality to create web requests in a fluent manner. It defines the available moments to which observers are subscribed for building the request.

npm install @pnp/queryable --save

"},{"location":"packages/#sp","title":"SP","text":"

This package provides a fluent SDK for calling SharePoint.

npm install @pnp/sp --save

"},{"location":"packages/#sp-admin","title":"SP-Admin","text":"

This package provides a fluent SDK for calling SharePoint tenant admin APIs

npm install @pnp/sp-admin --save

"},{"location":"transition-guide/","title":"Transition Guide","text":"

It is our hope that the transition from version 2.* to 3.* will be as painless as possible, however given the transition we have made from a global sp object to an instance based object some architectural and inital setup changes will need to be addressed. In the following sections we endevor to provide an overview of what changes will be required. If we missed something, please let us know in the issues list so we can update the guide. Thanks!

For a full, detailed list of what's been added, updated, and removed please see our CHANGELOG

For a full sample project, utilizing SPFx 1.14 and V3 that showcases some of the more dramatic changes to the library check out this sample.

"},{"location":"transition-guide/#benefits-and-advancements-in-v3","title":"Benefits and Advancements in V3","text":"

For version 2 the core themes were selective imports, a model based on factory functions & interfaces, and improving the docs. This foundation gave us the opportunity to re-write the entire request pipeline internals with minimal external library changes - showing a bit of long-term planning \ud83d\ude42. With version 3 your required updates are likely to only affect the initial configuration of the library, a huge accomplishment when updating the entire internals.

Our request pipeline remained largely unchanged since it was first written ~5 years ago, hard to change something so central to the library. The advantage of this update it is now easy for developers to inject their own logic into the request process. As always, this work was based on feedback over the years and understanding how we can be a better library. The new observer design allows you to customize every aspect of the request, in a much clearer way than was previously possible. In addition this work greatly reduced internal boilerplate code and optimized for library size. We reduced the size of sp & graph libraries by almost 2/3. As well we embraced a fully async design built around the new Timeline. Check out the new model around authoring observers and understand how they relate to moments. We feel this new architecture will allow far greater flexibility for consumers of the library to customize the behavior to exactly meet their needs.

We also used this as an opportunity to remove duplicate methods, clean up and improve our typings & method signatures, and drop legacy methods. Be sure to review the changelog. As always we do our best to minimize breaking changes but major versions are breaking versions.

We thank you for using the library. Your continued feedback drives these improvements, and we look forward to seeing what you build!

"},{"location":"transition-guide/#global-vs-instance-architecture","title":"Global vs Instance Architecture","text":"

The biggest change in version 3 of the library is the movement away from the globally defined sp and graph objects. Starting in version 2.1.0 we added the concept of Isolated Runtime which allowed you to create a separate instance of the global object that would have a separate configuration. We found that the implementation was finicky and prone to issues, so we have rebuilt the internals of the library from the ground up to better address this need. In doing so, we decided not to offer a global object at all.

Because of this change, any architecture that relies on the sp or graph objects being configured during initialization and then reused throughout the solution will need to be rethought. Essentially you have three options:

  1. Create a new spfi/graphfi object wherever it's required.
  2. Create a service architecture that can return a previously configured instance or utilize an instance and return the results
  3. Utilize a Project Preset file.

In other words, the sp and graph objects have been deprecated and will need to be replaced.

For more information on getting started with these new setup methods please see the Getting Started docs for a deeper look into the Queryable interface see Queryable.

"},{"location":"transition-guide/#assignfrom-and-copyfrom","title":"AssignFrom and CopyFrom","text":"

With the new Querable instance architecture we have provided a way to branch from one instance to another. To do this we provide two methods: AssignFrom and CopyFrom. These methods can be helpful when you want to establish a new instance to which you might apply other behaviors but want to reuse the configuration from a source. To learn more about them check out the Core/Behaviors documentation.

"},{"location":"transition-guide/#dropping-get","title":"Dropping \".get()\"","text":"

If you are still using the queryableInstance.get() method of queryable you must replace it with a direct invoke call queryableInstance().

"},{"location":"transition-guide/#batching","title":"Batching","text":"

Another benefit of the new updated internals is a significantly streamlined and simplified process for batching requests. Essentially, the interface for SP and Graph now function the same.

A new module called \"batching\" will need to be imported which then provides the batched interface which will return a tuple with a new Querable instance and an execute function. To see more details check out Batching.

"},{"location":"transition-guide/#web-spfi","title":"Web -> SPFI","text":"

In V2, to connect to a different web you would use the function

const web = Web({Other Web URL});\n

In V3 you would create a new instance of queryable connecting to the web of your choice. This new method provides you significantly more flexibility by not only allowing you to easily connect to other webs in the same tenant but also to webs in other tenants.

We are seeing a significant number of people report an error when using this method:

No observers registered for this request.

which results when it hasn't been updated to use the version 3 convention. Please see the examples below to pick the one that most suits your codebase.

import { spfi, SPFx } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\nconst spWebA = spfi().using(SPFx(this.context));\n\n// Easiest transition is to use the tuple pattern and the Web constructor which will copy all the observers from the object but set the url to the one provided\nconst spWebE = Web([spWebA.web, \"{Absolute URL of Other Web}\"]);\n\n// Create a new instance of Queryable\nconst spWebB = spfi(\"{Other Web URL}\").using(SPFx(this.context));\n\n// Copy/Assign a new instance of Queryable using the existing\nconst spWebC = spfi(\"{Other Web URL}\").using(AssignFrom(sp.web));\n\n// Create a new instance of Queryable using other credentials?\nconst spWebD = spfi(\"{Other Web URL}\").using(SPFx(this.context));\n\n

Please see the documentation for more information on the updated Web constructor.

"},{"location":"transition-guide/#dropping-commonjs-packages","title":"Dropping -Commonjs Packages","text":"

Starting with v3 we are dropping the commonjs versions of all packages. Previously we released these as we worked to transition to esm and the current node didn't yet support esm. With esm now a supported module type, and having done the work to ensure they work in node we feel it is a good time to drop the -commonjs variants. Please see documentation on Getting started with NodeJS Project using TypeScript producing CommonJS modules

"},{"location":"azidjsclient/","title":"@pnp/azidjsclient","text":"

This library provides a thin wrapper around the @azure/identity library to make it easy to integrate Azure Identity authentication in your solution.

You will first need to install the package:

npm install @pnp/azidjsclient --save

The following example shows how to configure the SPFI or GraphFI object using this behavior.

import { DefaultAzureCredential } from \"@azure/identity\";\nimport { spfi } from \"@pnp/sp\";\nimport { graphfi } from \"@pnp/sp\";\nimport { SPDefault, GraphDefault } from \"@pnp/nodejs\";\nimport { AzureIdentity } from \"@pnp/azidjsclient\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/graph/me\";\n\nconst credential = new DefaultAzureCredential();\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(\n    SPDefault(),\n    AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)\n);\n\nconst graph = graphfi().using(\n    GraphDefault(),\n    AzureIdentity(credential, [\"https://graph.microsoft.com/.default\"], null)\n);\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n

Please see more scenarios in the authentication article.

"},{"location":"concepts/adv-clientside-pages/","title":"Client-Side Pages","text":"

The client-side pages API included in this library is an implementation that was reverse engineered from the first-party API's and is unsupported by Microsoft. Given how flexible pages are we've done our best to give you the endpoints that will provide the functionality you need but that said, implementing these APIs is one of the more complicated tasks you can do.

It's especially important to understand the product team is constantly changing the features of pages and often that will also end up changing how the APIs that we've leveraged behave and because they are not offical third-party APIs this can cause our implementation to break. In order to fix those breaks we need to go back to the beginning and re-validate how the endpoints work searching for what has changed and then implementing those changes in our code. This is by no means simple. If you are reporting an issue with the pages API be aware that it may take significant time for us to unearth what is happening and fix it. Any research that you can provide when sharing your issue will go a long way in expediating that process, or better yet, if you can track it down and submit a PR with a fix we would be most greatful.

"},{"location":"concepts/adv-clientside-pages/#tricks-to-help-you-figure-out-how-to-add-first-party-web-parts-to-your-page","title":"Tricks to help you figure out how to add first-party web parts to your page","text":"

This section is to offer you methods to be able to reverse engineer some of the first party web parts to help figure out how to add them to the page using the addControl method.

Your first step needs to be creating a test page that you can inspect.

  1. Create a new Site Page.
  2. Open the browser console, and navigate to the network tab
  3. Filter the network tab for Fetch/XHR and then type SavePage to filter for the specific network calls.
  4. Add and configure the web part you want to reverse engineer and then save the page as draft. The network tab will now show a SavePageAsDraft call and you can then look at the Payload of that call
  5. You then want to specifically look at the CanvasContent1 property and copy that value. You can then paste it into a temporary file with the .json extension in your code editor so you can inspect the payload. The value is an array of objects, and each object (except the last) is the definition of the web part.

Below is an example (as of the last change date of this document) of what the QuickLinks web part looks like. One key takeaway from this file is the webPartId property which can be used when filtering for the right web part definition after getting a collection from sp.web.getClientsideWebParts();.

Note that it could change at any time so please do not rely on this data, please use it as an example only.

{\n    \"position\": {\n        \"layoutIndex\": 1,\n        \"zoneIndex\": 1,\n        \"sectionIndex\": 1,\n        \"sectionFactor\": 12,\n        \"controlIndex\": 1\n    },\n    \"controlType\": 3,\n    \"id\": \"00000000-58fd-448c-9e40-6691ce30e3e4\",\n    \"webPartId\": \"c70391ea-0b10-4ee9-b2b4-006d3fcad0cd\",\n    \"addedFromPersistedData\": true,\n    \"reservedHeight\": 141,\n    \"reservedWidth\": 909,\n    \"webPartData\": {\n        \"id\": \"c70391ea-0b10-4ee9-b2b4-006d3fcad0cd\",\n        \"instanceId\": \"00000000-58fd-448c-9e40-6691ce30e3e4\",\n        \"title\": \"Quick links\",\n        \"description\": \"Show a collection of links to content such as documents, images, videos, and more in a variety of layouts with options for icons, images, and audience targeting.\",\n        \"audiences\": [],\n        \"serverProcessedContent\": {\n            \"htmlStrings\": {},\n            \"searchablePlainTexts\": {\n                \"items[0].title\": \"PnPjs Title\"\n            },\n            \"imageSources\": {},\n            \"links\": {\n                \"baseUrl\": \"https://contoso.sharepoint.com/sites/PnPJS\",\n                \"items[0].sourceItem.url\": \"/sites/PnPJS/SitePages/pnpjsTestV2.aspx\"\n            },\n            \"componentDependencies\": {\n                \"layoutComponentId\": \"706e33c8-af37-4e7b-9d22-6e5694d92a6f\"\n            }\n        },\n        \"dataVersion\": \"2.2\",\n        \"properties\": {\n            \"items\": [\n                {\n                    \"sourceItem\": {\n                        \"guids\": {\n                            \"siteId\": \"00000000-4657-40d2-843d-3d6c72e647ff\",\n                            \"webId\": \"00000000-e714-4de6-88db-b0ac40d17850\",\n                            \"listId\": \"{00000000-8ED8-4E43-82BD-56794D9AB290}\",\n                            \"uniqueId\": \"00000000-6779-4979-adad-c120a39fe311\"\n                        },\n                        \"itemType\": 0,\n                        \"fileExtension\": \".ASPX\",\n                        \"progId\": null\n                    },\n                    \"thumbnailType\": 2,\n                    \"id\": 1,\n                    \"description\": \"\",\n                    \"fabricReactIcon\": {\n                        \"iconName\": \"heartfill\"\n                    },\n                    \"altText\": \"\",\n                    \"rawPreviewImageMinCanvasWidth\": 32767\n                }\n            ],\n            \"isMigrated\": true,\n            \"layoutId\": \"CompactCard\",\n            \"shouldShowThumbnail\": true,\n            \"imageWidth\": 100,\n            \"buttonLayoutOptions\": {\n                \"showDescription\": false,\n                \"buttonTreatment\": 2,\n                \"iconPositionType\": 2,\n                \"textAlignmentVertical\": 2,\n                \"textAlignmentHorizontal\": 2,\n                \"linesOfText\": 2\n            },\n            \"listLayoutOptions\": {\n                \"showDescription\": false,\n                \"showIcon\": true\n            },\n            \"waffleLayoutOptions\": {\n                \"iconSize\": 1,\n                \"onlyShowThumbnail\": false\n            },\n            \"hideWebPartWhenEmpty\": true,\n            \"dataProviderId\": \"QuickLinks\",\n            \"webId\": \"00000000-e714-4de6-88db-b0ac40d17850\",\n            \"siteId\": \"00000000-4657-40d2-843d-3d6c72e647ff\"\n        }\n    }\n}\n

At this point the only aspect of the above JSON payload you're going to be paying attention to is the webPartData. We have exposed title, description, and dataVersion as default properties of the ClientsideWebpart class. In addition we provide a getProperties, setProperties, getServerProcessedContent, setServerProcessedContent methods. The difference in this case in these set base methods is that it will merge the object you pass into those methods with the values already on the object.

The code below gives a incomplete but demonstrative example of how you would extend the ClientsideWebpart class to provide an interface to build a custom class for the QuickLinks web part illustrated in our JSON payload above. This code assumes you have already added the control to a section. For more information about that step see the documentation for Add Controls

import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ClientsideWebpart } from \"@pnp/sp/clientside-pages\";\n\n//Define interface based on JSON object above\ninterface IQLItem {\n    sourceItem: {\n        guids: {\n            siteId: string;\n            webId:  string;\n            listId:  string;\n            uniqueId:  string;\n        },\n        itemType: number;\n        fileExtension: string;\n        progId: string;\n    }\n    thumbnailType: number;\n    id: number;\n    description: string;\n    fabricReactIcon: { iconName: string; };\n    altText: string;\n    rawPreviewImageMinCanvasWidth: number;\n}\n\n// we create a class to wrap our functionality in a reusable way\nclass QuickLinksWebpart extends ClientsideWebpart {\n\n  constructor(control: ClientsideWebpart) {\n    super((<any>control).json);\n  }\n\n  // add property getter/setter for what we need, in this case items array within properties\n  public get items(): IQLItem[] {\n    return this.json.webPartData?.properties?.items || [];\n  }\n\n  public set items(value: IQLItem[]) {\n    this.json.webPartData.properties?.items = value;\n  }\n}\n\n// now we load our page\nconst page = await sp.web.loadClientsidePage(\"/sites/PnPJS/SitePages/QuickLinks-Web-Part-Test.aspx\");\n\n// get our part and pass it to the constructor of our wrapper class.\nconst part = new QuickLinksWebpart(page.sections[0].columns[0].getControl(0));\n\n//Need to set all the properties\npart.items = [{IQLItem_properties}];\n\nawait page.save();\n
"},{"location":"concepts/auth-browser/","title":"Authentication in a custom browser based application","text":"

We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.

npm install @pnp/msaljsclient --save

At this time we're using version 1.x of the msal library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.

Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:

import { Configuration, AuthenticationParameters } from \"msal\";\n\nconst configuration: Configuration = {\n  auth: {\n    authority: \"https://login.microsoftonline.com/{tenant Id}/\",\n    clientId: \"{AAD Application Id/Client Id}\"\n  }\n};\n\nconst authParams: AuthenticationParameters = {\n  scopes: [\"https://graph.microsoft.com/.default\"] \n};\n
"},{"location":"concepts/auth-browser/#msal-browser","title":"MSAL + Browser","text":"
import { spfi, SPBrowser } from \"@pnp/sp\";\nimport { graphfi, GraphBrowser } from \"@pnp/graph\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/graph/users\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPBrowser(), MSAL(configuration, authParams));\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst graph = graphfi().using(GraphBrowser(), MSAL(configuration, authParams));\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-nodejs/","title":"Authentication in NodeJS","text":"

We support MSAL for both browser and nodejs and Azure Identity for nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible configurations, but any parameters supplied are passed through to the underlying implementation.

Depending on which package you want to use you will need to install an additional package from the library because of the large dependencies.

We support MSAL through the msal-node library which is included by the @pnp/nodejs package.

For the Azure Identity package:

npm install @pnp/azidjsclient --save

We support Azure Identity through the @azure/identity library which simplifies the authentication process and makes it easy to integrate Azure Identity authentication in your solution.

"},{"location":"concepts/auth-nodejs/#msal-nodejs","title":"MSAL + NodeJS","text":"

The SPDefault and GraphDefault exported by the nodejs library include MSAL and takes the parameters directly.

The following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:

import { SPDefault, GraphDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\nimport { graphfi } from \"@pnp/graph\";\nimport { Configuration, AuthenticationParameters } from \"msal\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/sp/webs\";\n\nconst configuration: Configuration = {\n  auth: {\n    authority: \"https://login.microsoftonline.com/{tenant Id}/\",\n    clientId: \"{AAD Application Id/Client Id}\"\n  }\n};\n\nconst sp = spfi(\"{site url}\").using(SPDefault({\n    msal: {\n        config: configuration,\n        scopes: [\"https://{tenant}.sharepoint.com/.default\"],\n    },\n}));\n\nconst graph = graphfi().using(GraphDefault({\n    msal: {\n        config: configuration,\n        scopes: [\"https://graph.microsoft.com/.default\"],\n    },\n}));\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-nodejs/#use-nodejs-msal-behavior-directly","title":"Use Nodejs MSAL behavior directly","text":"

It is also possible to use the MSAL behavior directly if you are composing your own strategies.

import { SPDefault, GraphDefault, MSAL } from \"@pnp/nodejs\";\n\nconst sp = spfi(\"{site url}\").using(SPDefault(), MSAL({\n  config: configuration,\n  scopes: [\"https://{tenant}.sharepoint.com/.default\"],\n}));\n\nconst graph = graphfi().using(GraphDefault(), MSAL({\n  config: configuration,\n  scopes: [\"https://graph.microsoft.com/.default\"],\n}));\n\n
"},{"location":"concepts/auth-nodejs/#azure-identity-nodejs","title":"Azure Identity + NodeJS","text":"

The following sample shows how to pass the credential object to the AzureIdentity behavior including scopes.

import { DefaultAzureCredential } from \"@azure/identity\";\nimport { spfi } from \"@pnp/sp\";\nimport { graphfi } from \"@pnp/sp\";\nimport { SPDefault, GraphDefault } from \"@pnp/nodejs\";\nimport { AzureIdentity } from \"@pnp/azidjsclient\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/graph/users\";\n\n// We're using DefaultAzureCredential but the credential can be any valid `Credential Type`\nconst credential = new DefaultAzureCredential();\n\nconst sp = spfi(\"https://{tenant}.sharepoint.com/sites/dev\").using(\n    SPDefault(),\n    AzureIdentity(credential, [`https://${tenant}.sharepoint.com/.default`], null)\n);\n\nconst graph = graphfi().using(\n    GraphDefault(),\n    AzureIdentity(credential, [\"https://graph.microsoft.com/.default\"], null)\n);\n\nconst webData = await sp.web();\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-spfx/","title":"Authentication in SharePoint Framework","text":"

When building in SharePoint Framework you only need to provide the context to either sp or graph to ensure proper authentication. This will use the default SharePoint AAD application to manage scopes. If you would prefer to use a different AAD application please see the MSAL section below.

"},{"location":"concepts/auth-spfx/#spfx-sharepoint","title":"SPFx + SharePoint","text":"
import { SPFx, spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst sp = spfi().using(SPFx(this.context));\n\nconst webData = await sp.web();\n
"},{"location":"concepts/auth-spfx/#spfx-graph","title":"SPFx + Graph","text":"
import { SPFx, graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst graph = graphfi().using(SPFx(this.context));\n\nconst meData = await graph.me();\n
"},{"location":"concepts/auth-spfx/#spfx-authentication-token","title":"SPFx + Authentication Token","text":"

When using the SPFx behavior, authentication is handled by a cookie stored on the users client. In very specific instances some of the SharePoint methods will require a token. We have added a custom behavior to support that called SPFxToken. This will require that you add the appropriate application role to the SharePoint Framework's package-solution.json -> webApiPermissionRequests section where you will define the resource and scope for the request.

Here's an example of how you would build an instance of the SPFI that would include an Bearer Token in the header. Be advised if you use this instance to make calls to SharePoint endpoints that you have not specifically authorized they will fail.

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\n\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n
"},{"location":"concepts/auth-spfx/#msal-spfx","title":"MSAL + SPFx","text":"

We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency.

npm install @pnp/msaljsclient --save

At this time we're using version 1.x of the msal library which uses Implicit Flow. For more informaiton on the msal library please see the AzureAD/microsoft-authentication-library-for-js.

Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects:

import { SPFx as graphSPFx, graphfi } from \"@pnp/graph\";\nimport { SPFx as spSPFx, spfi } from \"@pnp/sp\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport { Configuration, AuthenticationParameters } from \"msal\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/sp/webs\";\n\nconst configuration: Configuration = {\n  auth: {\n    authority: \"https://login.microsoftonline.com/{tenant Id}/\",\n    clientId: \"{AAD Application Id/Client Id}\"\n  }\n};\n\nconst authParams: AuthenticationParameters = {\n  scopes: [\"https://graph.microsoft.com/.default\"] \n};\n\n// within a webpart, application customizer, or adaptive card extension where the context object is available\nconst graph = graphfi().using(graphSPFx(this.context), MSAL(configuration, authParams));\nconst sp = spfi().using(spSPFx(this.context), MSAL(configuration, authParams));\n\nconst meData = await graph.me();\nconst webData = await sp.web();\n
"},{"location":"concepts/authentication/","title":"Authentication","text":"

One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods.

We provide multiple ways to authenticate based on the scenario you're developing for, see one of these more detailed guides:

  • Authentication in SharePoint Framework
  • Authentication in a custom browser based application (Outside Microsoft 365)
  • Authentication in NodeJS

If you have more specific authentication requirements you can always build your own by using the new queryable pattern which exposes a dedicated auth moment. That moment expects observers with the signature:

async function(url, init) {\n\n  // logic to apply authentication to the request\n\n    return [url, init];\n}\n

You can follow this example as a general pattern to build your own custom authentication model. You can then wrap your authentication in a behavior for easy reuse.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using({behaviors});\nconst web = sp.web;\n\n// we will use custom auth on this web\nweb.on.auth(async function(url, init) {\n\n    // some code to get a token\n    const token = getToken();\n\n    // set the Authorization header in the init (this init is what is passed directly to the fetch call)\n    init.headers[\"Authorization\"] = `Bearer ${token}`;\n\n    return [url, init];\n});\n
"},{"location":"concepts/batching-caching/","title":"Batching and Caching","text":"

When optimizing for performance you can combine batching and caching to reduce the overall number of requests. On the first request any cachable data is stored as expected once the request completes. On subsequent requests if data is found in the cache it is returned immediately and that request is not added to the batch, in fact the batch will never register the request. This can work across many requests such that some returned cached data and others do not - the non-cached requests will be added to and processed by the batch as expected.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { Caching } from \"@pnp/queryable\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nbatchedSP.using(Caching());\n\nbatchedSP.web().then(console.log);\n\nbatchedSP.web.lists().then(console.log);\n\n// execute the first set of batched requests, no information is currently cached\nawait execute();\n\n// create a new batch\nconst [batchedSP2, execute2] = await sp.batched();\nbatchedSP2.using(Caching());\n\n// add the same requests - this simulates the user navigating away from or reloading the page\nbatchedSP2.web().then(console.log);\nbatchedSP2.web.lists().then(console.log);\n\n// executing the batch will return the cached values from the initial requests\nawait execute2();\n

In this second example we include an update to the web's title. Because non-get requests are never cached the update code will always run, but the results from the two get requests will resolve from the cache prior to being added to the batch.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { Caching } from \"@pnp/queryable\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nbatchedSP.using(Caching());\n\nbatchedSP.web().then(console.log);\n\nbatchedSP.web.lists().then(console.log);\n\n// this will never be cached\nbatchedSP.web.update({\n    Title: \"dev web 1\",\n});\n\n// execute the first set of batched requests, no information is currently cached\nawait execute();\n\n// create a new batch\nconst [batchedSP2, execute2] = await sp.batched();\nbatchedSP2.using(Caching());\n\n// add the same requests - this simulates the user navigating away from or reloading the page\nbatchedSP2.web().then(console.log);\nbatchedSP2.web.lists().then(console.log);\n\n// this will never be cached\nbatchedSP2.web.update({\n    Title: \"dev web 2\",\n});\n\n// executing the batch will return the cached values from the initial requests\nawait execute2();\n
"},{"location":"concepts/batching/","title":"Batching","text":"

Where possible batching can significantly increase application performance by combining multiple requests to the server into one. This is especially useful when first establishing state, but applies for any scenario where you need to make multiple requests before loading or based on a user action. Batching is supported within the sp and graph libraries as shown below.

"},{"location":"concepts/batching/#sp-example","title":"SP Example","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/batching\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nlet res = [];\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\nbatchedSP.web().then(r => res.push(r));\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\n// ODATA operations such as select, filter, and expand are supported as normal\nbatchedSP.web.lists.select(\"Title\")().then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i = 0; i < res.length; i++) {\n    ///Do something with the results\n}\n
"},{"location":"concepts/batching/#using-a-batched-web","title":"Using a batched web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/batching\";\n\nconst sp = spfi(...);\n\nconst [batchedWeb, execute] = sp.web.batched();\n\nlet res = [];\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\nbatchedWeb().then(r => res.push(r));\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\n// ODATA operations such as select, filter, and expand are supported as normal\nbatchedWeb.lists.select(\"Title\")().then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i = 0; i < res.length; i++) {\n    ///Do something with the results\n}\n

Batches must be for the same web, you cannot combine requests from multiple webs into a batch.

"},{"location":"concepts/batching/#graph-example","title":"Graph Example","text":"
import { graphfi } from \"@pnp/graph\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/batching\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\nconst [batchedGraph, execute] = graph.batched();\n\nlet res = [];\n\n// Pushes the results of these calls to an array\n// you need to use .then syntax here as otherwise the application will stop and await the result\nbatchedGraph.users().then(r => res.push(r));\n\n// you need to use .then syntax here as otherwise the application will stop and await the result\n// ODATA operations such as select, filter, and expand are supported as normal\nbatchedGraph.groups.select(\"Id\")().then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i=0; i<res.length; i++){\n    // Do something with the results\n}\n
"},{"location":"concepts/batching/#advanced-batching","title":"Advanced Batching","text":"

For most cases the above usage should be sufficient, however you may be in a situation where you do not have convenient access to either an spfi instance or a web. Let's say for example you want to add a lot of items to a list and have an IList. You can in these cases use the createBatch function directly. We recommend as much as possible using the sp or web or graph batched method, but also provide this additional flexibility if you need it.

import { createBatch } from \"@pnp/sp/batching\";\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { IList } from \"@pnp/sp/lists\";\nimport \"@pnp/sp/items/list\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPDefault({ /* ... */ }));\n\n// in one part of your application you setup a list instance\nconst list: IList = sp.web.lists.getByTitle(\"MyList\");\n\n\n// in another part of your application you want to batch requests, but do not have the sp instance available, just the IList\n\n// note here the first part of the tuple is NOT the object, rather the behavior that enables batching. You must still register it with `using`.\nconst [batchedListBehavior, execute] = createBatch(list);\n// this list is now batching all its requests\nlist.using(batchedListBehavior);\n\n// these will all occur within a single batch\nlist.items.add({ Title: `1: ${getRandomString(4)}` });\nlist.items.add({ Title: `2: ${getRandomString(4)}` });\nlist.items.add({ Title: `3: ${getRandomString(4)}` });\nlist.items.add({ Title: `4: ${getRandomString(4)}` });\n\nawait execute();\n

This is of course also possible with the graph library as shown below.

import { graphfi } from \"@pnp/graph\";\nimport { createBatch } from \"@pnp/graph/batching\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\nconst users = graph.users;\n\nconst [batchedBehavior, execute] = createBatch(users);\nusers.using(batchedBehavior);\n\nusers();\n// we can only place the 'users' instance into the batch once\ngraph.users.using(batchedBehavior)();\ngraph.users.using(batchedBehavior)();\ngraph.users.using(batchedBehavior)();\n\nawait execute();       \n

"},{"location":"concepts/batching/#dont-reuse-objects-in-batching","title":"Don't reuse objects in Batching","text":"

It shouldn't come up often, but you can not make multiple requests using the same instance of a queryable in a batch. Let's consider the incorrect example below:

The error message will be \"This instance is already part of a batch. Please review the docs at https://pnp.github.io/pnpjs/concepts/batching#reuse.\"

import { graphfi } from \"@pnp/graph\";\nimport { createBatch } from \"@pnp/graph/batching\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\n// gain a batched instance of the graph\nconst [batchedGraph, execute] = graph.batched();\n\n// we take a reference to the value returned from .users\nconst users = batchedGraph.users;\n\n// we invoke it, adding it to the batch (this is a request to /users), it will succeed\nusers();\n\n// we invoke it again, because this instance has already been added to the batch, this request will throw an error\nusers();\n\n// we execute the batch, this promise will resolve\nawait execute();        \n

To overcome this you can either start a new fluent chain or use the factory method. Starting a new fluent chain at any point will create a new instance. Please review the corrected sample below.

import { graphfi } from \"@pnp/graph\";\nimport { createBatch } from \"@pnp/graph/batching\";\nimport { GraphDefault } from \"@pnp/nodejs\";\nimport { Users } from \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphDefault({ /* ... */ }));\n\n// gain a batched instance of the graph\nconst [batchedGraph, execute] = graph.batched();\n\n// we invoke a new instance of users from the batchedGraph\nbatchedGraph.users();\n\n// we again invoke a new instance of users from the batchedGraph, this is fine\nbatchedGraph.users();\n\nconst users = batchedGraph.users;\n// we can do this once\nusers();\n\n// by creating a new users instance using the Users factory we can keep adding things to the batch\n// users2 will be part of the same batch\nconst users2 = Users(users);\nusers2();\n\n// we execute the batch, this promise will resolve\nawait execute();        \n

In addition you cannot continue using a batch after execute. Once execute has resolved the batch is done. You should create a new batch using one of the described methods to conduct another batched call.

"},{"location":"concepts/batching/#case-where-batch-result-returns-an-object-that-can-be-invoked","title":"Case where batch result returns an object that can be invoked","text":"

In the following example, the results of adding items to the list is an object with a type of IItemAddResult which is {data: any, item: IItem}. Since version v1 the expectation is that the item object is immediately usable to make additional queries. When this object is the result of a batched call, this was not the case so we have added additional code to reset the observers using the original base from witch the batch was created, mimicing the behavior had the IItem been created from that base withyout a batch involved. We use CopyFrom to ensure that we maintain the references to the InternalResolve and InternalReject events through the end of this timelines lifecycle.

import { createBatch } from \"@pnp/sp/batching\";\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { IList } from \"@pnp/sp/lists\";\nimport \"@pnp/sp/items/list\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPDefault({ /* ... */ }));\n\n// in one part of your application you setup a list instance\nconst list: IList = sp.web.lists.getByTitle(\"MyList\");\n\nconst [batchedListBehavior, execute] = createBatch(list);\n// this list is now batching all its requests\nlist.using(batchedListBehavior);\n\nlet res: IItemAddResult[] = [];\n\n// these will all occur within a single batch\nlist.items.add({ Title: `1: ${getRandomString(4)}` }).then(r => res.push(r));\nlist.items.add({ Title: `2: ${getRandomString(4)}` }).then(r => res.push(r));\nlist.items.add({ Title: `3: ${getRandomString(4)}` }).then(r => res.push(r));\nlist.items.add({ Title: `4: ${getRandomString(4)}` }).then(r => res.push(r));\n\nawait execute();\n\nlet newItems: IItem[] = [];\n\nfor(let i=0; i<res.length; i++){\n    //This line will correctly resolve\n    const newItem = await res[i].item.select(\"Title\")<{Title: string}>();\n    newItems.push(newItem);\n}\n
"},{"location":"concepts/calling-other-endpoints/","title":"Calling other endpoints not currently implemented in PnPjs library","text":"

If you find that there are endpoints that have not yet been implemented, or have changed in such a way that there are issues using the implemented endpoint, you can still make those calls and take advantage of the plumbing provided by the library.

"},{"location":"concepts/calling-other-endpoints/#sharepoint","title":"SharePoint","text":"

To issue calls against the SharePoint REST endpoints you would use one of the existing operations:

  • spGet
  • spPost
  • spDelete
  • spPatch and the extended post methods with additional headers.
  • spPostMerge
  • spPostDelete
  • spPostDeleteETag

To construct a call you will need to pass, to the operation call an SPQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.

Below are a couple of examples to get you started.

"},{"location":"concepts/calling-other-endpoints/#example-spget","title":"Example spGet","text":"

Let's pretend that the getById method didn't exist on a lists items. The example below shows two methods for constructing our SPQueryable method.

The first is the easiest to use because, as the queryable documentation tells us, this will maintain all the registered observers on the original queryable instance. We would start with the queryable object closest to the endpoint we want to use, in this case list. We do this because we need to construct the full URL that will be called. Using list in this instance gives us the first part of the URL (e.g. https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')) and then we can construct the remainder of the call by passing in a string.

The second method essentially starts from scratch where the user constructs the entire url and then registers observers on the SPQuerable instance. Then uses spGet to execute the call. There are many other variations to arrive at the same outcome, all are dependent on your requirements.

import { spfi } from \"@pnp/sp\";\nimport { AssignFrom } from \"@pnp/core\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { spGet, SPQueryable, SPFx } from \"@pnp/sp\";\n\n// Establish SPFI instance passing in the appropriate behavior to register the initial observers.\nconst sp = spfi(...);\n\n// create an instance of the items queryable\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// get the item with an id of 1, easiest method\nconst item: any = await spGet(SPQueryable(list, \"items(1)\"));\n\n// get the item with an id of 1, constructing a new queryable and registering behaviors\nconst spQueryable = SPQueryable(\"https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)\").using(SPFx(this.context));\n\n// ***or***\n\n// For v3 the full url is require for SPQuerable when providing just a string\nconst spQueryable = SPQueryable(\"https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)\").using(AssignFrom(sp.web));\n\n// and then use spQueryable to make the request\nconst item: any = await spGet(spQueryable);\n

The resulting call will be to the endpoint: https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/items(1)

"},{"location":"concepts/calling-other-endpoints/#example-sppost","title":"Example spPost","text":"

Let's now pretend that we need to get the changes on a list and want to call the getchanges method off list.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { IChangeQuery, spPost, SPQueryable } from \"@pnp/sp\";\nimport { body } from \"@pnp/queryable\";\n\n// Establish SPFI instance passing in the appropriate behavior to register the initial observers.\nconst sp = spfi(...);\n\n\n// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore\nconst query: IChangeQuery = {\n    Add: true,\n    ChangeTokenEnd: null,\n    ChangeTokenStart: null,\n    DeleteObject: true,\n    Rename: true,\n    Restore: true,\n};\n\n// create an instance of the items queryable\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// get the item with an id of 1\nconst changes: any = await spPost(SPQueryable(list, \"getchanges\"), body({query}));\n\n

The resulting call will be to the endpoint: https://contoso.sharepoint.com/sites/testsite/_api/web/lists/getByTitle('My List')/getchanges

"},{"location":"concepts/calling-other-endpoints/#microsoft-graph","title":"Microsoft Graph","text":"

To issue calls against the Microsoft Graph REST endpoints you would use one of the existing operations:

  • graphGet
  • graphPost
  • graphDelete
  • graphPatch
  • graphPut

To construct a call you will need to pass, to the operation call an GraphQueryable and optionally a RequestInit object which will be merged with any existing registered init object. To learn more about queryable and the options for constructing one, check out the documentation.

Below are a couple of examples to get you started.

"},{"location":"concepts/calling-other-endpoints/#example-graphget","title":"Example graphGet","text":"

Here's an example for getting the chats for a particular user. This uses the simplest method for constructing the graphQueryable which is to start with a instance of a queryable that is close to the endpoint we want to call, in this case user and then adding the additional path as a string. For a more advanced example see spGet above.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport { GraphQueryable, graphGet } from \"@pnp/graph\";\n\n// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.\nconst graph = graphfi(...);\n\n// create an instance of the user queryable\nconst user = graph.users.getById('jane@contoso.com');\n\n// get the chats for the user\nconst chat: any = await graphGet(GraphQueryable(user, \"chats\"));\n

The results call will be to the endpoint: https://graph.microsoft.com/v1.0/users/jane@contoso.com/chats

"},{"location":"concepts/calling-other-endpoints/#example-graphpost","title":"Example graphPost","text":"

This is an example of adding an event to a calendar.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/calendars\";\nimport { GraphQueryable, graphPost } from \"@pnp/graph\";\nimport { body, InjectHeaders } from \"@pnp/queryable\";\n\n// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.\nconst graph = graphfi(...);\n\n// create an instance of the user queryable\nconst calendar = graph.users.getById('jane@contoso.com').calendar;\n\nconst props = {\n  \"subject\": \"Let's go for lunch\",\n  \"body\": {\n    \"contentType\": \"HTML\",\n    \"content\": \"Does noon work for you?\"\n  },\n  \"start\": {\n      \"dateTime\": \"2017-04-15T12:00:00\",\n      \"timeZone\": \"Pacific Standard Time\"\n  },\n  \"end\": {\n      \"dateTime\": \"2017-04-15T14:00:00\",\n      \"timeZone\": \"Pacific Standard Time\"\n  },\n  \"location\":{\n      \"displayName\":\"Harry's Bar\"\n  },\n  \"attendees\": [\n    {\n      \"emailAddress\": {\n        \"address\":\"samanthab@contoso.onmicrosoft.com\",\n        \"name\": \"Samantha Booth\"\n      },\n      \"type\": \"required\"\n    }\n  ],\n  \"allowNewTimeProposals\": true,\n  \"transactionId\":\"7E163156-7762-4BEB-A1C6-729EA81755A7\"\n};\n\n// custom request init to add timezone header.\nconst graphQueryable = GraphQueryable(calendar, \"events\").using(InjectHeaders({\n    \"Prefer\": 'outlook.timezone=\"Pacific Standard Time\"',\n}));\n\n// adds a new event to the user's calendar\nconst event: any = await graphPost(graphQueryable, body(props));\n

The results call will be to the endpoint: https://graph.microsoft.com/v1.0/users/jane@contoso.com/calendar/events

"},{"location":"concepts/calling-other-endpoints/#advanced-scenario","title":"Advanced Scenario","text":"

If you find you need to create an instance of Queryable (for either graph or SharePoint) that would hang off the root of the url you can use the AssignFrom or CopyFrom behaviors.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport { GraphQueryable, graphPost } from \"@pnp/graph\";\nimport { body, InjectHeaders } from \"@pnp/queryable\";\nimport { AssignFrom } from \"@pnp/core\";\n\n// Establish GRAPHFI instance passing in the appropriate behavior to register the initial observers.\nconst graph = graphfi(...);\n\nconst chatsQueryable = GraphQueryable(\"chats\").using(AssignFrom(graph.me));\n\nconst chat: any = await graphPost(chatsQueryable, body(chatBody));\n

The results call will be to the endpoint: https://graph.microsoft.com/v1.0/chats

"},{"location":"concepts/custom-bundle/","title":"Custom Bundling","text":"

With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles.

Scenarios could include:

  • Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once.
  • Creating SPFx libraries either for one project or a single webpart.
  • Create a single library containing the PnPjs code you need bundled along with your custom extensions.
"},{"location":"concepts/custom-bundle/#create-a-custom-bundle","title":"Create a custom bundle","text":""},{"location":"concepts/custom-bundle/#webpack","title":"Webpack","text":"

You can see/clone a sample project of this example here.

"},{"location":"concepts/error-handling/","title":"Error Handling","text":"

This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.

For 429, 503, and 504 errors we include retry logic within the library

"},{"location":"concepts/error-handling/#the-httprequesterror","title":"The HttpRequestError","text":"

All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error. In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible.

Property Name Description name Standard Error.name property. Always 'Error' message Normalized string containing the status, status text, and the full response text stack The callstack producing the error isHttpRequestError Always true, allows you to reliably determine if you have an HttpRequestError instance response Unread copy of the Response object resulting in the thrown error status The Response.status value (such as 404) statusText The Response.statusText value (such as 'Not Found')"},{"location":"concepts/error-handling/#basic-handling","title":"Basic Handling","text":"

For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen \ud83d\ude09. The most basic type of error handling involves a simple try-catch when using the async/await promises pattern.

import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\ntry {\n\n  // get a list that doesn't exist\n  const w = await sp.web.lists.getByTitle(\"no\")();\n\n} catch (e) {\n\n  console.error(e);\n}\n

This will produce output like:

Error making HttpClient request in queryable [404] Not Found ::> {\"odata.error\":{\"code\":\"-1, System.ArgumentException\",\"message\":{\"lang\":\"en-US\",\"value\":\"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'.\"}}} Data: {\"response\":{\"size\":0,\"timeout\":0},\"status\":404,\"statusText\":\"Not Found\",\"isHttpRequestError\":true}\n

This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.

"},{"location":"concepts/error-handling/#reading-the-response","title":"Reading the Response","text":"

In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire:

import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { HttpRequestError } from \"@pnp/queryable\";\n\ntry {\n\n  // get a list that doesn't exist\n  const w = await sp.web.lists.getByTitle(\"no\")();\n\n} catch (e) {\n\n  // are we dealing with an HttpRequestError?\n  if (e?.isHttpRequestError) {\n\n    // we can read the json from the response\n    const json = await (<HttpRequestError>e).response.json();\n\n    // if we have a value property we can show it\n    console.log(typeof json[\"odata.error\"] === \"object\" ? json[\"odata.error\"].message.value : e.message);\n\n    // add of course you have access to the other properties and can make choices on how to act\n    if ((<HttpRequestError>e).status === 404) {\n       console.error((<HttpRequestError>e).statusText);\n      // maybe create the resource, or redirect, or fallback to a secondary data source\n      // just ideas, handle any of the status codes uniquely as needed\n    }\n\n  } else {\n    // not an HttpRequestError so we just log message\n    console.log(e.message);\n  }\n}\n
"},{"location":"concepts/error-handling/#logging-errors","title":"Logging errors","text":"

Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework.

import { Logger } from \"@pnp/logging\";\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\ntry {\n  // get a list that doesn't exist\n  const w = await sp.web.lists.getByTitle(\"no\")();  \n} catch (e) {\n\n  Logger.error(e);\n}\n

You may want to read the response and customize the message as described above:

import { Logger } from \"@pnp/logging\";\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { HttpRequestError } from \"@pnp/queryable\";\n\ntry {\n  // get a list that doesn't exist\n  const w = await sp.web.lists.getByTitle(\"no\")();  \n} catch (e) {\n\n  if (e?.isHttpRequestError) {\n\n    // we can read the json from the response\n    const data = await (<HttpRequestError>e).response.json();\n\n    // parse this however you want\n    const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message;\n\n    // we use the status to determine a custom logging level\n    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;\n\n    // create a custom log entry\n    Logger.log({\n      data,\n      level,\n      message,\n    });\n\n  } else {\n    // not an HttpRequestError so we just log message\n    Logger.error(e);\n  }\n}\n
"},{"location":"concepts/error-handling/#putting-it-all-together","title":"Putting it All Together","text":"

After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.

"},{"location":"concepts/error-handling/#errorhandlerts","title":"errorhandler.ts","text":"
import { Logger } from \"@pnp/logging\";\nimport { HttpRequestError } from \"@pnp/queryable\";\nimport { hOP } from \"@pnp/core\";\n\nexport async function handleError(e: Error | HttpRequestError): Promise<void> {\n\n  if (hOP(e, \"isHttpRequestError\")) {\n\n    // we can read the json from the response\n    const data = await (<HttpRequestError>e).response.json();\n\n    // parse this however you want\n    const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message;\n\n    // we use the status to determine a custom logging level\n    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;\n\n    // create a custom log entry\n    Logger.log({\n      data,\n      level,\n      message,\n    });\n\n  } else {\n    // not an HttpRequestError so we just log message\n    Logger.error(e);\n  }\n}\n
"},{"location":"concepts/error-handling/#web-requestts","title":"web-request.ts","text":"
import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { handleError } from \"./errorhandler\";\n\ntry {\n\n  const w = await sp.web.lists.getByTitle(\"no\")();\n\n} catch (e) {\n\n  await handleError(e);\n}\n
"},{"location":"concepts/error-handling/#web-request2ts","title":"web-request2.ts","text":"
import { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { handleError } from \"./errorhandler\";\n\ntry {\n\n  const w = await sp.web.lists();\n\n} catch (e) {\n\n  await handleError(e);\n}\n
"},{"location":"concepts/error-handling/#building-a-custom-error-handler","title":"Building a Custom Error Handler","text":"

In Version 3 the library introduced the concept of a Timeline object and moments. One of the broadcast moments is error. To create your own custom error handler you can define a special handler for the error moment something like the following:

\n//Custom Error Behavior\nexport function CustomError(): TimelinePipe<Queryable> {\n\n    return (instance: Queryable) => {\n\n        instance.on.error((err) => {\n            if (logging) {\n                console.log(`\ud83d\uded1 PnPjs Testing Error - ${err.toString()}`);\n            }\n        });\n\n        return instance;\n    };\n}\n\n//Adding our CustomError behavior to our timline\n\nconst sp = spfi().using(SPDefault(this.context)).using(CustomError());\n
"},{"location":"concepts/invokable/","title":"Invokables","text":"

For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain: Starting with v3 this is no longer possible, you must invoke the object directly to execute the default action for that class:

const lists = await sp.web.lists();\n
"},{"location":"concepts/nightly-builds/","title":"Nightly Builds","text":"

Starting with version 3 we support nightly builds, which are built from the version-3 branch each evening and include all the changes merged ahead of a particular build. These are a great way to try out new features before a release, or get a fix or enhancement without waiting for the monthly builds.

You can install the nightly builds using the below examples. While we only show examples for sp and graph nightly builds are available for all packages.

"},{"location":"concepts/nightly-builds/#sp","title":"SP","text":"
npm install @pnp/sp@v3nightly --save\n
"},{"location":"concepts/nightly-builds/#microsoft-graph","title":"Microsoft Graph","text":"
npm install @pnp/graph@v3nightly --save\n

Nightly builds are NOT monthly releases and aren't tested as deeply. We never intend to release broken code, but nightly builds may contain some code that is not entirely final or fully reviewed. As always if you encounter an issue please let us know, especially for nightly builds so we can be sure to address it before the next monthly release.

"},{"location":"concepts/project-preset/","title":"Project Config/Services Setup","text":"

Due to the introduction of selective imports it can be somewhat frustrating to import all of the needed dependencies every time you need them across many files. Instead the preferred approach, especially for SPFx, is to create a project config file or establish a service to manage your PnPjs interfaces. Doing so centralizes the imports, configuration, and optionally extensions to PnPjs in a single place.

If you have multiple projects that share dependencies on PnPjs you can benefit from creating a custom bundle and using them across your projects.

These steps reference an SPFx solution, but apply to any solution.

"},{"location":"concepts/project-preset/#using-a-config-file","title":"Using a config file","text":"

Within the src directory create a new file named pnpjs-config.ts and copy in the below content.

import { WebPartContext } from \"@microsoft/sp-webpart-base\";\n\n// import pnp, pnp logging system, and any other selective imports needed\nimport { spfi, SPFI, SPFx } from \"@pnp/sp\";\nimport { LogLevel, PnPLogging } from \"@pnp/logging\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/batching\";\n\nvar _sp: SPFI = null;\n\nexport const getSP = (context?: WebPartContext): SPFI => {\n  if (context != null) {\n    //You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency\n    // The LogLevel set's at what level a message will be written to the console\n    _sp = spfi().using(SPFx(context)).using(PnPLogging(LogLevel.Warning));\n  }\n  return _sp;\n};\n

To initialize the configuration, from the onInit function (or whatever function runs first in your code) make a call to getSP passing in the SPFx context object (or whatever configuration you would require for your setup).

protected async onInit(): Promise<void> {\n  this._environmentMessage = this._getEnvironmentMessage();\n\n  super.onInit();\n\n  //Initialize our _sp object that we can then use in other packages without having to pass around the context.\n  //  Check out pnpjsConfig.ts for an example of a project setup file.\n  getSP(this.context);\n}\n

Now you can consume your configured _sp object from anywhere else in your code by simply referencing the pnpjs-presets.ts file via an import statement and then getting a local instance of the _sp object using the getSP() method without passing any context.

import { getSP } from './pnpjs-config.ts';\n...\nexport default class PnPjsExample extends React.Component<IPnPjsExampleProps, IIPnPjsExampleState> {\n\n  private _sp: SPFI;\n\n  constructor(props: IPnPjsExampleProps) {\n    super(props);\n    // set initial state\n    this.state = {\n      items: [],\n      errors: []\n    };\n    this._sp = getSP();\n  }\n\n  ...\n\n}\n
"},{"location":"concepts/project-preset/#use-a-service-class","title":"Use a service class","text":"

Because you do not have full access to the context object within a service you need to setup things a little differently.

import { ServiceKey, ServiceScope } from \"@microsoft/sp-core-library\";\nimport { PageContext } from \"@microsoft/sp-page-context\";\nimport { AadTokenProviderFactory } from \"@microsoft/sp-http\";\nimport { spfi, SPFI, SPFx as spSPFx } from \"@pnp/sp\";\nimport { graphfi, GraphFI, SPFx as gSPFx } from \"@pnp/graph\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\nexport interface ISampleService {\n    getLists(): Promise<any[]>;\n}\n\nexport class SampleService {\n\n    public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService);\n    private _sp: SPFI;\n    private _graph: GraphFI;\n\n    constructor(serviceScope: ServiceScope) {\n\n        serviceScope.whenFinished(() => {\n\n        const pageContext = serviceScope.consume(PageContext.serviceKey);\n        const aadTokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);\n\n        //SharePoint\n        this._sp = spfi().using(spSPFx({ pageContext }));\n\n        //Graph\n        this._graph = graphfi().using(gSPFx({ aadTokenProviderFactory }));\n    }\n\n    public getLists(): Promise<any[]> {\n        return this._sp.web.lists();\n    }\n}\n

Depending on the architecture of your solution you can also opt to export the service as a global. If you choose this route you would need to modify the service to create an Init function where you would pass the service scope instead of doing so in the constructor. You would then export a constant that creates a global instance of the service.

export const mySampleService = new SampleService();\n

For a full sample, please see our PnPjs Version 3 Sample Project

"},{"location":"concepts/selective-imports/","title":"Selective Imports","text":"

As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking.

This concept works well with custom bundling to create a shared package tailored exactly to your needs.

If you would prefer to not worry about selective imports please see the section on presets.

A quick note on how TypeScript handles type only imports. If you have a line like import { IWeb } from \"@pnp/sp/webs\" everything will transpile correctly but you will get runtime errors because TS will see that line as a type only import and drop it. You need to include both import { IWeb } from \"@pnp/sp/webs\" and import \"@pnp/sp/webs\" to ensure the webs functionality is correctly included. You can see this in the last example below.

// the sp var now has almost nothing attached at import time and relies on\n\n// we need to import each of the pieces we need to \"attach\" them for chaining\n// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items/list\";\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\nconst itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();\n

Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific

// the sp var now has almost nothing attached at import time and relies on\n\n// we need to import each of the pieces we need to \"attach\" them for chaining\n// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\nconst itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();\n

The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example:

// this import statement will attach content-type functionality to list, web, and item\nimport \"@pnp/sp/content-types\";\n\n// this import statement will only attach content-type functionality to web\nimport \"@pnp/sp/content-types/web\";\n

If you only need to access content types on the web object you can reduce size by only importing that piece.

The below example shows the need to import types and module augmentation separately.

// this will fail\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists\";\n\n// do this instead\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport { IList } from \"@pnp/sp/lists\";\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\nconst lists = await sp.web.lists();\n
"},{"location":"concepts/selective-imports/#presets","title":"Presets","text":"

Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually. Both libraries supply an \"all\" preset that will attach all of the available library functionality.

While the presets provided may be useful, we encourage you to look at making your own project presets or custom bundles as a preferred solution. Use of the presets in client-side solutions is not recommended.

"},{"location":"concepts/selective-imports/#sp","title":"SP","text":"
import \"@pnp/sp/presets/all\";\n\n\n// placeholder for fully configuring the sp interface\nconst sp = spfi();\n\n// sp.* will have all of the library functionality bound to it, tree shaking will not work\nconst lists = await sp.web.lists();\n
"},{"location":"concepts/selective-imports/#graph","title":"Graph","text":"

The graph library contains a single preset, \"all\" mimicking the v1 structure.

import \"@pnp/graph/presets/all\";\nimport { graphfi } from \"@pnp/graph\";\n\n// placeholder for fully configuring the sp interface\nconst graph = graphfi();\n\n// graph.* will have all of the library functionality bound to it, tree shaking will not work\nconst me = await graph.me();\n
"},{"location":"concepts/typings/","title":"Typing Return Objects","text":"

Whenever you make a request of the library for data from an object and utilize the select method to reduce the size of the objects in the payload its preferable in TypeScript to be able to type that returned object. The library provides you a method to do so by using TypeScript's Generics declaration.

By defining the objects type in the <> after the closure of the select method the resulting object is typed.

  .select(\"Title\")<{Title: string}>()\n

Below are some examples of typing the return payload:

  const _sp = spfi().using(SPFx(this.context));\n\n  //Typing the Title property of a field\n  const field = await _sp.site.rootWeb.fields.getById(titleFieldId).select(\"Title\")<{ Title: string }>();\n\n  //Typing the ParentWebUrl property of the selected list.\n  const testList = await _sp.web.lists.getByTitle('MyList').select(\"ParentWebUrl\")<{ ParentWebUrl: string }>();\n

There have been discussions in the past around auto-typing based on select and the expected properties of the return object. We haven't done so for a few reasons: there is no even mildly complex way to account for all the possibilities expand introduces to selects, and if we \"ignore\" expand it effectively makes the select typings back to \"any\". Looking at template types etc, we haven't yet seen a way to do this that makes it worth the effort and doesn't introduce some other limitation or confusion.

"},{"location":"contributing/","title":"Contributing to PnPjs","text":"

Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality.

Section Description NPM Scripts Explains the npm scripts and their uses Setup Dev Machine Covers setting up your machine to ensure you are ready to debug the solution Local Debug Configuration Discusses the steps required to establish local configuration used for debugging and running tests Debugging Describes how to debug PnPjs locally Extending the library Basic examples on how to extend the library such as adding a method or property Writing Tests How to write and debug tests Update Documentation Describes the steps required to edit and locally view the documentation Submit a Pull Request Outlines guidance for submitting a pull request"},{"location":"contributing/#need-help","title":"Need Help?","text":"

The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.

Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.

To learn more and register for an upcoming session, please visit the Sharing is Caring website.

"},{"location":"contributing/debug-tests/","title":"Writing Tests","text":"

With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.

"},{"location":"contributing/debug-tests/#how-to-write-tests","title":"How to write Tests","text":"

We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts:

import { getRandomString } from \"@pnp/core\";\nimport { testSettings } from \"../main\";\nimport { expect } from \"chai\";\nimport { sp } from \"@pnp/sp\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items/list\";\nimport { IList } from \"@pnp/sp/lists\";\n\ndescribe(\"Items\", () => {\n\n    // any tests that make a web request should be withing a block checking if web tests are enabled\n    if (testSettings.enableWebTests) {\n\n        // a block scoped var we will use across our tests\n        let list: IList = null;\n\n        // we use the before block to setup\n        // executed before all the tests in this block, see the mocha docs for more details\n        // mocha prefers using function vs arrow functions and this is recommended\n        before(async function () {\n\n            // execute a request to ensure we have a list\n            const ler = await sp.web.lists.ensure(\"ItemTestList\", \"Used to test item operations\");\n            list = ler.list;\n\n            // in this case we want to have some items in the list for testing so we add those\n            // only if the list was just created\n            if (ler.created) {\n\n                // add a few items to get started\n                const batch = sp.web.createBatch();\n                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });\n                await batch.execute();\n            }\n        });\n\n        // this test has a label \"get items\" and is run via an async function\n        it(\"get items\", async function () {\n\n            // make a request for the list's items\n            const items = await list.items();\n\n            // report that we expect that result to be an array with more than 0 items\n            expect(items.length).to.be.gt(0);\n        });\n\n        // ... remainder of code removed\n    }\n}\n
"},{"location":"contributing/debug-tests/#general-guidelines-for-writing-tests","title":"General Guidelines for Writing Tests","text":"
  • Tests should operate within the site defined in testSettings
  • Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves
  • Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll
  • When writing tests you can use \"only\" and \"skip\" from mochajs to focus on only the tests you are writing
  • Be sure to review the various options when running your tests
  • If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description
"},{"location":"contributing/debug-tests/#next-steps","title":"Next Steps","text":"

Now that you've written tests to cover your changes you'll need to update the docs.

"},{"location":"contributing/debugging/","title":"Debugging","text":"

Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on.

Before proceeding be sure you have reviewed how to setup for local configuration and debugging.

"},{"location":"contributing/debugging/#debugging-library-features","title":"Debugging Library Features","text":"

The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.

"},{"location":"contributing/debugging/#basic-sharepoint-testing","title":"Basic SharePoint Testing","text":"

You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules.

All of the setup for the node client is handled within sp.ts using the settings from the local configuration.

"},{"location":"contributing/debugging/#basic-graph-testing","title":"Basic Graph Testing","text":"

Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit.

All of the setup for the node client is handled within graph.ts using the settings from the local configuration.

"},{"location":"contributing/debugging/#how-to-create-a-debug-module","title":"How to: Create a Debug Module","text":"

If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git.

Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content:

// note we can use the actual package names for our imports (ex: @pnp/logging)\nimport { Logger, LogLevel, ConsoleListener } from \"@pnp/logging\";\n// using the all preset for simplicity in the example, selective imports work as expected\nimport { sp, ListEnsureResult } from \"@pnp/sp/presets/all\";\n\ndeclare var process: { exit(code?: number): void };\n\nexport async function MyDebug() {\n\n  // configure your options\n  // you can have different configs in different modules as needed for your testing/dev work\n  sp.setup({\n    sp: {\n      fetchClientFactory: () => {\n        return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret);\n      },\n    },\n  });\n\n  // run some debugging\n  const list = await sp.web.lists.ensure(\"MyFirstList\");\n\n  Logger.log({\n    data: list.created,\n    level: LogLevel.Info,\n    message: \"Was list created?\",\n  });\n\n  if (list.created) {\n\n    Logger.log({\n      data: list.data,\n      level: LogLevel.Info,\n      message: \"Raw data from list creation.\",\n    });\n\n  } else {\n\n    Logger.log({\n      data: null,\n      level: LogLevel.Info,\n      message: \"List already existed!\",\n    });\n  }\n\n  process.exit(0);\n}\n
"},{"location":"contributing/debugging/#update-maints-to-launch-your-module","title":"Update main.ts to launch your module","text":"

First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this:

// ...\n\n// comment out the example\n// import { Example } from \"./example\";\n// Example();\n\nimport { MyDebug } from \"./mydebug\"\nMyDebug();\n\n// ...\n

Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)

"},{"location":"contributing/debugging/#debug","title":"Debug","text":"

Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.

"},{"location":"contributing/debugging/#debug-module-next-steps","title":"Debug Module Next Steps","text":"

Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.

"},{"location":"contributing/debugging/#in-browser-debugging","title":"In Browser Debugging","text":"

You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.

"},{"location":"contributing/debugging/#start-the-local-serve","title":"Start the local serve","text":"

This will serve a package with ./debug/serve/main.ts as the entry.

npm run serve

"},{"location":"contributing/debugging/#add-reference-to-library","title":"Add reference to library","text":"

Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.

<script src=\"https://localhost:8080/assets/pnp.js\"></script>\n<div id=\"pnp-test\"></div>\n

You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.

"},{"location":"contributing/debugging/#debug_1","title":"Debug","text":"

Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.

"},{"location":"contributing/debugging/#next-steps","title":"Next Steps","text":"

You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser.

Now you can learn about extending the library.

"},{"location":"contributing/documentation/","title":"Documentation","text":"

Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.

"},{"location":"contributing/documentation/#writing-docs","title":"Writing Docs","text":"

Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.

"},{"location":"contributing/documentation/#building-docs-locally","title":"Building Docs Locally","text":"

Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.

When executing the pip module on Windows you can prefix it with python -m. For example:

python -m pip install mkdocs-material

  • Install MkDocs
    • pip install mkdocs
  • Install the Material theme
    • pip install mkdocs-material
  • install the mkdocs-markdownextradata-plugin - this is used for the version variable
    • pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7)
  • install redirect plugin - used to redirect from moved pages
    • pip install mkdocs-redirects
  • Serve it up
    • mkdocs serve
    • Open a browser to http://127.0.0.1:8000/

Please see the official mkdocs site for more details on working with mkdocs

"},{"location":"contributing/documentation/#next-steps","title":"Next Steps","text":"

After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request!

"},{"location":"contributing/extending-the-library/","title":"Extending PnPjs","text":"

This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property.

At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the \"webs\" property is added to the web class.

// TypeScript property, returning an interface\npublic get webs(): IWebs {\n    // using the Webs factory function and providing \"this\" as the first parameter\n    return Webs(this);\n}\n
"},{"location":"contributing/extending-the-library/#understanding-factory-functions","title":"Understanding Factory Functions","text":"

PnPjs v3 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form.

// create a constant which is a function of type ISPInvokableFactory having the name Webs\n// this is bound by the generic type param to return an IWebs instance\n// and it will use the _Webs concrete class to form the internal type of the invocable\nexport const Webs = spInvokableFactory<IWebs>(_Webs);\n

The ISPInvokableFactory type looks like:

export type ISPInvokableFactory<R = any> = (baseUrl: string | ISharePointQueryable, path?: string) => R;\n

And the matching graph type:

<R>(f: any): (baseUrl: string | IGraphQueryable, path?: string) => R\n

The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples.

import { SPFx } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\n// create a web from an absolute url\nconst web = Web(\"https://tenant.sharepoint.com\").using(SPFx(this.context));\n\n// as an example, create a new web using the first as a base\n// targets: https://tenant.sharepoint.com/sites/dev\nconst web2 = Web(web, \"sites/dev\");\n\n// or you can add any path components you want, here as an example we access the current user property\nconst cu = Web(web, \"currentuser\");\nconst currentUserInfo = cu();\n

Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their \"type\". It is an important concept when working with the library to always remember we are just building strings.

"},{"location":"contributing/extending-the-library/#class-structure","title":"Class structure","text":"

Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method

/*\nThe concrete class implementation. This is never exported or shown directly\nto consumers of the library. It is wrapped by the Proxy we do expose.\n\nIt extends the _SharePointQueryableInstance class for which there is a matching\n_SharePointQueryableCollection. The generic parameter defines the return type\nof a get operation and the invoked result.\n\nClasses can have methods and properties as normal. This one has a single property as a simple example\n*/\nexport class _HubSite extends _SharePointQueryableInstance<IHubSiteInfo> {\n\n    /**\n     * Gets the ISite instance associated with this hub site\n     */\n    // the tag decorator is used to provide some additional telemetry on what methods are\n    // being called.\n    @tag(\"hs.getSite\")\n    public async getSite(): Promise<ISite> {\n\n        // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result\n        const d = await this.select(\"SiteUrl\")();\n\n        // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl\n        return Site(d.SiteUrl);\n    }\n}\n\n/*\nThis defines the interface we export and expose to consumers.\nIn most cases this extends the concrete object but may add or remove some methods/properties\nin special cases\n*/\nexport interface IHubSite extends _HubSite { }\n\n/*\nThis defines the HubSite factory function as discussed above\nbinding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite.\n\nThis is understood to mean that HubSite is a factory function that returns a types of IHubSite\nwhich the spInvokableFactory will create using _HubSite as the concrete underlying type.\n*/\nexport const HubSite = spInvokableFactory<IHubSite>(_HubSite);\n
"},{"location":"contributing/extending-the-library/#add-a-property","title":"Add a Property","text":"

In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class.

export class _View extends _SharePointQueryableInstance<IViewInfo> {\n\n    // ... other code removed\n\n    // add the property, and provide a return type\n    // return types should be interfaces\n    public get fields(): IViewFields {\n        // we use the ViewFields factory function supplying \"this\" as the first parameter\n        // this will create a url like \".../fields/viewfields\" due to the defaultPath decorator\n        // on the _ViewFields class. This is equivalent to: ViewFields(this, \"viewfields\")\n        return ViewFields(this);\n    }\n\n    // ... other code removed\n}\n

There are many examples throughout the library that follow this pattern.

"},{"location":"contributing/extending-the-library/#add-a-method","title":"Add a Method","text":"

Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method:

@defaultPath(\"items\")\nexport class _Items extends _SharePointQueryableCollection {\n\n    /**\n    * Gets an Item by id\n    *\n    * @param id The integer id of the item to retrieve\n    */\n    // we declare a method and set the return type to an interface\n    public getById(id: number): IItem {\n        // here we use the tag helper to add some telemetry to our request\n        // we create a new IItem using the factory and appending the id value to the end\n        // this gives us a valid url path to a single item .../items/getById(2)\n        // we can then use the returned IItem to extend our chain or execute a request\n        return tag.configure(Item(this).concat(`(${id})`), \"is.getById\");\n    }\n\n    // ... other code removed\n}\n
"},{"location":"contributing/extending-the-library/#web-request-method","title":"Web Request Method","text":"

A second example is a method that performs a request. Here we use the _Item recycle method as an example:

/**\n * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item.\n */\n// we use the tag decorator to add telemetry\n@tag(\"i.recycle\")\n// we return a promise\npublic recycle(): Promise<string> {\n    // we use the spPost method to post the request created by cloning our current instance IItem using\n    // the Item factory and adding the path \"recycle\" to the end. Url will look like .../items/getById(2)/recycle\n    return spPost<string>(Item(this, \"recycle\"));\n}\n
"},{"location":"contributing/extending-the-library/#augment-using-selective-imports","title":"Augment Using Selective Imports","text":"

To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available.

// import the addProp helper\nimport { addProp } from \"@pnp/queryable\";\n// import the _List concrete class from the types module (not the index!)\nimport { _List } from \"../lists/types\";\n// import the interface and factory we are going to add to the List\nimport { Items, IItems } from \"./types\";\n\n// This module declaration fixes up the types, allowing .items to appear in intellisense\n// when you import \"@pnp/sp/items/list\";\ndeclare module \"../lists/types\" {\n    // we need to extend the concrete type\n    interface _List {\n        readonly items: IItems;\n    }\n    // we need to extend the interface\n    // this may not be strictly necessary as the IList interface extends _List so it\n    // should pick up the same additions, but we have seen in some cases this does seem\n    // to be required. So we include it for safety as it will all be removed during\n    // transpilation we don't need to care about the extra code\n    interface IList {\n        readonly items: IItems;\n    }\n}\n\n// finally we add the property to the _List class\n// this method call says add a property to _List named \"items\" and that property returns a result using the Items factory\n// The factory will be called with \"this\" when the property is accessed. If needed there is a fourth parameter to append additional path\n// information to the property url\naddProp(_List, \"items\", Items);\n
"},{"location":"contributing/extending-the-library/#general-rules-for-extending-pnpjs","title":"General Rules for Extending PnPjs","text":"
  • Only expose interfaces to consumers
  • Use the factory functions except in very special cases
  • Look for other properties and methods as examples
  • Simple is always preferable, but not always possible - use your best judgement
  • If you find yourself writing a ton of code to solve a problem you think should be easy, ask
  • If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed
"},{"location":"contributing/extending-the-library/#next-steps","title":"Next Steps","text":"

Now that you have extended the library you need to write a test to cover it!

"},{"location":"contributing/local-debug-configuration/","title":"Local Debugging Configuration","text":"

This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).

"},{"location":"contributing/local-debug-configuration/#create-settingsjs","title":"Create settings.js","text":"

Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. For more information the settings file please see Settings

"},{"location":"contributing/local-debug-configuration/#minimal-configuration","title":"Minimal Configuration","text":"

You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag.

The following configuration file allows you to run all the tests that do not contact services.

 var sets = {\n     testing: {\n         enableWebTests: false,\n     }\n }\n\nmodule.exports = sets;\n
"},{"location":"contributing/local-debug-configuration/#test-your-setup","title":"Test your setup","text":"

If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.

"},{"location":"contributing/npm-scripts/","title":"Supported NPM Scripts","text":"

As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies.

This article outlines the current scripts we've implemented and how to use them, with available options and examples.

"},{"location":"contributing/npm-scripts/#start","title":"Start","text":"

Executes the serve command

npm start\n
"},{"location":"contributing/npm-scripts/#serve","title":"Serve","text":"

Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node.

npm run serve\n
"},{"location":"contributing/npm-scripts/#test","title":"Test","text":"

Runs the tests and coverage for the library.

More details on setting up MSAL for node.

"},{"location":"contributing/npm-scripts/#options","title":"Options","text":"

There are several options you can provide to the test command. All of these need to be separated using a \"--\" double hyphen so they are passed to the spawned sub-commands.

"},{"location":"contributing/npm-scripts/#test-a-single-package","title":"Test a Single Package","text":"

--package or -p

This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory.

# run only sp tests\nnpm test -- -p sp\n\n# run only logging tests\nnpm test -- -package logging\n
"},{"location":"contributing/npm-scripts/#run-a-single-test-file","title":"Run a Single Test File","text":"

--single or --s

You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags.

# run only sp web tests\nnpm test -- -p sp -s web\n\n# run only graph groups tests\nnpm test -- -package graph -single groups\n
"},{"location":"contributing/npm-scripts/#specify-a-site","title":"Specify a Site","text":"

--site

By default every time you run the tests a new sub-site is created below the site specified in your settings file. You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option.

This option can be used with any or none of the other testing options.

# run only sp web tests with a certain site\nnpm test -- -p sp -s web --site https://some.site.com/sites/dev\n
"},{"location":"contributing/npm-scripts/#cleanup","title":"Cleanup","text":"

--cleanup

If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted.

# clean up our testing site\nnpm test -- --cleanup\n
"},{"location":"contributing/npm-scripts/#logging","title":"Logging","text":"

--logging

If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output.

# enable logging during testing\nnpm test -- --logging\n

You can also optionally set a log level of error, warning, info, or verbose:

# enable logging during testing in verbose (lots of info)\nnpm test -- --logging verbose\n
# enable logging during testing in error\nnpm test -- --logging error\n
"},{"location":"contributing/npm-scripts/#spverbose","title":"spVerbose","text":"

--spverbose

This flag will enable \"verbose\" OData mode for SharePoint tests. This flag is compatible with other flags.

npm test -- --spverbose\n
"},{"location":"contributing/npm-scripts/#build","title":"build","text":"

Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed.

npm run build\n
"},{"location":"contributing/npm-scripts/#package","title":"package","text":"

Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published.

npm run package\n
"},{"location":"contributing/npm-scripts/#lint","title":"lint","text":"

Runs the linter.

npm run lint\n
"},{"location":"contributing/npm-scripts/#clean","title":"clean","text":"

Removes any generated folders from the working directory.

npm run clean\n
"},{"location":"contributing/pull-requests/","title":"Submitting Pull Requests","text":"

Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release.

  • Target your pull requests to the version-3 branch
  • Add/Update any relevant docs articles in the relevant package's docs folder related to your changes
  • Include a test for any new functionality and ensure all existing tests are passing by running npm test
  • Ensure linting checks pass by typing npm run lint
  • Ensure everything works for a build by running npm run package
  • Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work
  • If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :)

If you need to target a PR for version 1, please target the \"version-1\" branch

"},{"location":"contributing/pull-requests/#sharing-is-caring-pull-request-guidance","title":"Sharing is Caring - Pull Request Guidance","text":"

The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.

Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.

To learn more and register for an upcoming session, please visit the Sharing is Caring website.

"},{"location":"contributing/pull-requests/#next-steps","title":"Next Steps","text":"

Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted.

Thank you for helping PnPjs grow and improve!!

"},{"location":"contributing/settings/","title":"Project Settings","text":"

This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally.

The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root.

"},{"location":"contributing/settings/#settings-file-format","title":"Settings File Format","text":"

The settings file is configured with MSAL authentication for both SharePoint and Graph. For more information coinfiguring MSAL please review the section in the authentication section for node.

MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always \"https://{tenant}.sharepoint.com/.default\" or \"https://graph.microsoft.com/.default\" depending on what you are calling.

If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated.

You will need to create testing certs for the sample settings file below. Using the following code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration. Copy the contents of the \"key.pem\" file and paste it in the privateKey variable below. The gitignore file in this repository will ignore the settings.js file.

Replace HereIsMySuperPass with your own password

mkdir \\temp\ncd \\temp\nopenssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'\nopenssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass\n
const privateKey = `-----BEGIN RSA PRIVATE KEY-----\nyour private key, read from a file or included here\n-----END RSA PRIVATE KEY-----\n`;\n\nvar msalInit = {\n    auth: {\n        authority: \"https://login.microsoftonline.com/{tenant id}\",\n        clientCertificate: {\n            thumbprint: \"{certificate thumbnail}\",\n            privateKey: privateKey,\n        },\n        clientId: \"{AAD App registration id}\",\n    }\n}\n\nexport const settings = {\n    testing: {\n        enableWebTests: true,\n        testUser: \"i:0#.f|membership|user@consto.com\",\n        testGroupId:\"{ Microsoft 365 Group ID }\",\n        sp: {\n            url: \"{required for MSAL - absolute url of test site}\",\n            notificationUrl: \"{ optional: notification url }\",\n            msal: {\n                init: msalInit,\n                scopes: [\"https://{tenant}.sharepoint.com/.default\"]\n            },\n        },\n        graph: {\n            msal: {\n                init: msalInit,\n                scopes: [\"https://graph.microsoft.com/.default\"]\n            },\n        },\n    },\n}\n\n

The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below.

enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. testUser AAD login account to be used when running tests. testGroupId Group ID of Microsoft 365 Group to be used when running test cases. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests"},{"location":"contributing/settings/#sp-values","title":"SP values","text":"name description url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions msal Information about MSAL authentication setup"},{"location":"contributing/settings/#graph-value","title":"Graph value","text":"

The graph values are described in the table below and come from registering an AAD Application. The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against.

name description msal Information about MSAL authentication setup"},{"location":"contributing/settings/#create-settingsjs-file","title":"Create Settings.js file","text":"
  1. Copy the example file and rename it settings.js. Place the file in the root of your project.
  2. Update the settings as needed for your environment.

If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.

"},{"location":"contributing/setup-dev-machine/","title":"Setting up your Developer Machine","text":"

If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging.

"},{"location":"contributing/setup-dev-machine/#setup-your-development-environment","title":"Setup your development environment","text":"

These steps will help you get your environment setup for contributing to the core library.

  1. Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like.

  2. Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).

    This library requires node >= 10.18.0

  3. On Windows: Install Python

  4. [Optional] Install the tslint extension in VS Code:

    1. Press Shift + Ctrl + \"p\" to open the command panel
    2. Begin typing \"install extension\" and select the command when it appears in view
    3. Begin typing \"tslint\" and select the package when it appears in view
    4. Restart Code after installation
"},{"location":"contributing/setup-dev-machine/#fork-the-repo","title":"Fork The Repo","text":"

All of our contributions come via pull requests and you'll need to fork the repository

  1. Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.

  2. Once you have the code locally, navigate to the root of the project in your console. Type the following command:

    npm install

  3. Follow the guidance to complete the one-time local configuration required to debug and run tests.

  4. Then you can follow the guidance in the debugging article.

"},{"location":"core/behavior-recipes/","title":"Behavior Recipes","text":"

This article contains example recipes for building your own behaviors. We don't want to include every possible behavior within the library, but do want folks to have easy ways to solve the problems they encounter. If have ideas for a missing recipe, please let us know in the issues list OR submit them to this page as a PR! We want to see what types of behaviors folks build and will evaluate options to either include them in the main libraries, leave them here as a reference resource, or possibly release a community behaviors package.

Alternatively we encourage you to publish your own behaviors as npm packages to share with others!

"},{"location":"core/behavior-recipes/#proxy","title":"Proxy","text":"

At times you might need to introduce a proxy for requests for debugging or other networking needs. You can easily do so using your proxy of choice in Nodejs. This example uses \"https-proxy-agent\" but would work similarly for any implementation.

proxy.ts

import { TimelinePipe } from \"@pnp/core\";\nimport { Queryable } from \"@pnp/queryable\";\nimport { HttpsProxyAgent } from \"https-proxy-agent\";\n\nexport function Proxy(proxyInit: string): TimelinePipe<Queryable>;\n// eslint-disable-next-line no-redeclare\nexport function Proxy(proxyInit: any): TimelinePipe<Queryable>;\n// eslint-disable-next-line no-redeclare\nexport function Proxy(proxyInit: any): TimelinePipe<Queryable> {\n\n    const proxy = typeof proxyInit === \"string\" ? new HttpsProxyAgent(proxyInit) : proxyInit;\n\n    return (instance: Queryable) => {\n\n        instance.on.pre(async (url, init, result) => {\n\n            // we add the proxy to the request\n            (<any>init).agent = proxy;\n\n            return [url, init, result];\n        });\n\n        return instance;\n    };\n}\n

usage

import { Proxy } from \"./proxy.ts\";\n\nimport \"@pnp/sp/webs\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\n// would work with graph library in the same manner\nconst sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault({\n    msal: {\n        config: { config },\n        scopes: {scopes },\n    },\n}), Proxy(\"http://127.0.0.1:8888\"));\n\nconst webInfo = await sp.webs();\n
"},{"location":"core/behavior-recipes/#add-querystring-to-bypass-request-caching","title":"Add QueryString to bypass request caching","text":"

In some instances users express a desire to append something to the querystring to avoid getting cached responses back for requests. This pattern is an example of doing that in v3.

query-cache-param.ts

export function CacheBust(): TimelinePipe<Queryable> {\n\n    return (instance: Queryable) => {\n\n        instance.on.pre(async (url, init, result) => {\n\n            url += url.indexOf(\"?\") > -1 ? \"&\" : \"?\";\n\n            url += \"nonce=\" + encodeURIComponent(new Date().toISOString());\n\n            return [url, init, result];\n        });\n\n        return instance;\n    };\n}\n

usage

import { CacheBust } from \"./query-cache-param.ts\";\n\nimport \"@pnp/sp/webs\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\n// would work with graph library in the same manner\nconst sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault({\n    msal: {\n        config: { config },\n        scopes: { scopes },\n    },\n}), CacheBust());\n\nconst webInfo = await sp.webs();\n
"},{"location":"core/behavior-recipes/#acs-authentication","title":"ACS Authentication","text":"

Starting with v3 we no longer provide support for ACS authentication within the library. However you may have a need (legacy applications, on-premises) to use ACS authentication while wanting to migrate to v3. Below you can find an example implementation of an Authentication observer for ACS. This is not a 100% full implementation, for example the tokens are not cached.

Whenever possible we encourage you to use AAD authentication and move away from ACS for securing your server-side applications.

export function ACS(clientId: string, clientSecret: string, authUrl = \"https://accounts.accesscontrol.windows.net\"): (instance: Queryable) => Queryable {\n\n  const SharePointServicePrincipal = \"00000003-0000-0ff1-ce00-000000000000\";\n\n  async function getRealm(siteUrl: string): Promise<string> {\n\n    const url = combine(siteUrl, \"_vti_bin/client.svc\");\n\n    const r = await nodeFetch(url, {\n      \"headers\": {\n        \"Authorization\": \"Bearer \",\n      },\n      \"method\": \"POST\",\n    });\n\n    const data: string = r.headers.get(\"www-authenticate\") || \"\";\n    const index = data.indexOf(\"Bearer realm=\\\"\");\n    return data.substring(index + 14, index + 50);\n  }\n\n  function getFormattedPrincipal(principalName: string, hostName: string, realm: string): string {\n    let resource = principalName;\n    if (hostName !== null && hostName !== \"\") {\n      resource += \"/\" + hostName;\n    }\n    resource += \"@\" + realm;\n    return resource;\n  }\n\n  async function getFullAuthUrl(realm: string): Promise<string> {\n\n    const url = combine(authUrl, `/metadata/json/1?realm=${realm}`);\n\n    const r = await nodeFetch(url, { method: \"GET\" });\n    const json: { endpoints: { protocol: string; location: string }[] } = await r.json();\n\n    const eps = json.endpoints.filter(ep => ep.protocol === \"OAuth2\");\n    if (eps.length > 0) {\n      return eps[0].location;\n    }\n\n    throw Error(\"Auth URL Endpoint could not be determined from data.\");\n  }\n\n  return (instance: Queryable) => {\n\n    instance.on.auth.replace(async (url: URL, init: RequestInit) => {\n\n      const realm = await getRealm(url.toString());\n      const fullAuthUrl = await getFullAuthUrl(realm);\n\n      const resource = getFormattedPrincipal(SharePointServicePrincipal, url.host, realm);\n      const formattedClientId = getFormattedPrincipal(clientId, \"\", realm);\n\n      const body: string[] = [];\n      body.push(\"grant_type=client_credentials\");\n      body.push(`client_id=${formattedClientId}`);\n      body.push(`client_secret=${encodeURIComponent(clientSecret)}`);\n      body.push(`resource=${resource}`);\n\n      const r = await nodeFetch(fullAuthUrl, {\n        body: body.join(\"&\"),\n        headers: {\n          \"Content-Type\": \"application/x-www-form-urlencoded\",\n        },\n        method: \"POST\",\n      });\n\n      const accessToken: { access_token: string } = await r.json();\n\n      init.headers = { ...init.headers, Authorization: `Bearer ${accessToken.access_token}` };\n\n      return [url, init];\n    });\n\n    return instance;\n  };\n}\n

usage

import { CacheBust } from \"./acs-auth-behavior.ts\";\nimport \"@pnp/sp/webs\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault(), ACS(\"{client id}\", \"{client secret}\"));\n\n// you can optionally provide the authentication url, here using the one for China's sovereign cloud or an local url if working on-premises\n// const sp = spfi(\"https://tenant.sharepoint.com/sites.dev\").using(SPDefault(), ACS(\"{client id}\", \"{client secret}\", \"https://accounts.accesscontrol.chinacloudapi.cn\"));\n\nconst webInfo = await sp.webs();\n
"},{"location":"core/behaviors/","title":"@pnp/core : behaviors","text":"

While you can always register observers to any Timeline's moments using the .on.moment syntax, to make things easier we have included the ability to create behaviors. Behaviors define one or more observer registrations abstracted into a single registration. To differentiate behaviors are applied with the .using method. The power of behaviors is they are composable so a behavior can apply other behaviors.

"},{"location":"core/behaviors/#basic-example","title":"Basic Example","text":"

Let's create a behavior that will register two observers to a Timeline. We'll use error and log since they exist on all Timelines. In this example let's imagine we need to include some special secret into every lifecycle for logging to work. And we also want a company wide method to track errors. So we roll our own behavior.

import { Timeline, TimelinePipe } from \"@pnp/core\";\nimport { MySpecialLoggingFunction } from \"../mylogging.js\";\n\n// top level function allows binding of values within the closure\nexport function MyBehavior(specialSecret: string): TimelinePipe {\n\n    // returns the actual behavior function that is applied to the instance\n    return (instance: Timeline<any>) => {\n\n        // register as many observers as needed\n        instance.on.log(function (message: string, severity: number) {\n\n            MySpecialLoggingFunction(message, severity, specialSecret);\n        });\n\n        instance.on.error(function (err: string | Error) {\n\n            MySpecialLoggingFunction(typeof err === \"string\" ? err : err.toString(), severity, specialSecret);\n        });\n\n        return instance;\n    };\n}\n\n// apply the behavior to a Timeline/Queryable\nobj.using(MyBehavior(\"HereIsMySuperSecretValue\"));\n
"},{"location":"core/behaviors/#composing-behaviors","title":"Composing Behaviors","text":"

We encourage you to use our defaults, or create your own default behavior appropriate to your needs. You can see all of the behaviors available in @pnp/nodejs, @pnp/queryable, @pnp/sp, and @pnp/graph.

As an example, let's create our own behavior for a nodejs project. We want to call the graph, default to the beta endpoint, setup MSAL, and include a custom header we need for our environment. To do so we create a composed behavior consisting of graph's DefaultInit, graph's DefaultHeaders, nodejs's MSAL, nodejs's NodeFetchWithRetry, and queryable's DefaultParse & InjectHeaders. Then we can import this behavior into all our projects to configure them.

company-default.ts

import { TimelinePipe } from \"@pnp/core\";\nimport { DefaultParse, Queryable, InjectHeaders } from \"@pnp/queryable\";\nimport { DefaultHeaders, DefaultInit } from \"@pnp/graph\";\nimport { NodeFetchWithRetry, MSAL } from \"@pnp/nodejs\";\n\nexport function CompanyDefault(): TimelinePipe<Queryable> {\n\n    return (instance: Queryable) => {\n\n        instance.using(\n            // use the default headers\n            DefaultHeaders(),\n            // use the default init, but change the base url to beta\n            DefaultInit(\"https://graph.microsoft.com/beta\"),\n            // use node-fetch with retry\n            NodeFetchWithRetry(),\n            // use the default parsing\n            DefaultParse(),\n            // inject our special header to all requests\n            InjectHeaders({\n                \"X-SomeSpecialToken\": \"{THE SPECIAL TOKEN VALUE}\",\n            }),\n            // setup node's MSAL with configuration from the environment (or any source)\n            MSAL(process.env.MSAL_CONFIG));\n\n        return instance;\n    };\n}\n

index.ts

import { CompanyDefault } from \"./company-default.ts\";\nimport { graphfi } from \"@pnp/graph\";\n\n// we can consistently and easily setup our graph instance using a single behavior\nconst graph = graphfi().using(CompanyDefault());\n

You can easily share your composed behaviors across your projects using library components in SPFx, a company CDN, or an npm package.

"},{"location":"core/behaviors/#core-behaviors","title":"Core Behaviors","text":"

This section describes two behaviors provided by the @pnp/core library, AssignFrom and CopyFrom. Likely you won't often need them directly - they are used in some places internally - but they are made available should they prove useful.

"},{"location":"core/behaviors/#assignfrom","title":"AssignFrom","text":"

This behavior creates a ref to the supplied Timeline implementation's observers and resets the inheriting flag. This means that changes to the parent, here being the supplied Timeline, will begin affecting the target to which this behavior is applied.

import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { AssignFrom } from \"@pnp/core\";\n// some local project file\nimport { MyCustomeBehavior } from \"./behaviors.ts\";\n\nconst source = spfi().using(SPBrowser());\n\nconst target = spfi().using(MyCustomeBehavior());\n\n// target will now hold a reference to the observers contained in source\n// changes to the subscribed observers in source will apply to target\n// anything that was added by \"MyCustomeBehavior\" will no longer be present\ntarget.using(AssignFrom(source.web));\n\n// you can always apply additional behaviors or register directly on the events\n// but once you modify target it will not longer ref source and changes to source will no longer apply\ntarget.using(SomeOtherBehavior());\ntarget.on.log(console.log);\n
"},{"location":"core/behaviors/#copyfrom","title":"CopyFrom","text":"

Similar to AssignFrom, this method creates a copy of all the observers on the source and applies them to the target. This can be done either as a replace or append operation using the second parameter. The default is \"append\".

  • \"replace\" will first clear each source moment's registered observers then apply each in source-order via the on operation.
  • \"append\" will apply each source moment's registered observers in source-order via the on operation

By design CopyFrom does NOT include moments defined by symbol keys.

import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { CopyFrom } from \"@pnp/core\";\n// some local project file\nimport { MyCustomeBehavior } from \"./behaviors.ts\";\n\nconst source = spfi().using(SPBrowser());\n\nconst target = spfi().using(MyCustomeBehavior());\n\n// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are maintained as the default behavior is to append\ntarget.using(CopyFrom(source.web));\n\n// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are removed\ntarget.using(CopyFrom(source.web, \"replace\"));\n\n// you can always apply additional behaviors or register directly on the events\n// with CopyFrom no reference to source is maintained\ntarget.using(SomeOtherBehavior());\ntarget.on.log(console.log);\n

As well CopyFrom supports a filter parameter if you only want to copy the observers from a subset of moments. This filter is a predicate function taking a single string key and returning true if the observers from that moment should be copied to the target.

import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { CopyFrom } from \"@pnp/core\";\n// some local project file\nimport { MyCustomeBehavior } from \"./behaviors.ts\";\n\nconst source = spfi().using(SPBrowser());\n\nconst target = spfi().using(MyCustomeBehavior());\n\n// target will have the observers copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are maintained as the default behavior is to append\ntarget.using(CopyFrom(source.web));\n\n// target will have the observers `auth` and `send` copied from source, but no reference to source. Changes to source's registered observers will not affect target.\n// any previously registered observers in target are removed\ntarget.using(CopyFrom(source.web, \"replace\", (k) => /(auth|send)/i.test(k)));\n\n// you can always apply additional behaviors or register directly on the events\n// with CopyFrom no reference to source is maintained\ntarget.using(SomeOtherBehavior());\ntarget.on.log(console.log);\n
"},{"location":"core/moments/","title":"@pnp/core : moments","text":"

Moments are the name we use to describe the steps executed during a timeline lifecycle. They are defined on a plain object by a series of functions with the general form:

// the first argument is the set of observers subscribed to the given moment\n// the rest of the args vary by an interaction between moment and observer types and represent the args passed when emit is called for a given moment\nfunction (observers: any[], ...args: any[]): any;\n

Let's have a look at one of the included moment factory functions, which define how the moment interacts with its registered observers, and use it to understand a bit more on how things work. In this example we'll look at the broadcast moment, used to mimic a classic event where no return value is tracked, we just want to emit an event to all the subscribed observers.

// the broadcast factory function, returning the actual moment implementation function\n// The type T is used by the typings of Timeline to described the arguments passed in emit\nexport function broadcast<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {\n\n    // this is the actual moment implementation, called each time a given moment occurs in the timeline\n    return function (observers: T[], ...args: any[]): void {\n\n        // we make a local ref of the observers\n        const obs = [...observers];\n\n        // we loop through sending the args to each observer\n        for (let i = 0; i < obs.length; i++) {\n\n            // note that within every moment and observer \"this\" will be the current timeline object\n            Reflect.apply(obs[i], this, args);\n        }\n    };\n}\n

Let's use broadcast in a couple examples to show how it works. You can also review the timeline article for a fuller example.

// our first type determines the type of the observers that will be regsitered to the moment \"first\"\ntype Broadcast1ObserverType = (this: Timeline<any>, message: string) => void;\n\n// our second type determines the type of the observers that will be regsitered to the moment \"second\"\ntype Broadcast2ObserverType = (this: Timeline<any>, value: number, value2: number) => void;\n\nconst moments = {\n    first: broadcast<Broadcast1ObserverType>(),\n    second: broadcast<Broadcast2ObserverType>(),\n} as const;\n

Now that we have defined two moments we can update our Timeline implementing class to emit each as we desire, as covered in the timeline article. Let's focus on the relationship between the moment definition and the typings inherited by on and emit in Timeline.

Because we want observers of a given moment to understand what arguments they will get the typings of Timeline are setup to use the type defining the moment's observer across all operations. For example, using our moment \"first\" from above. Each moment can be subscribed by zero or more observers.

// our observer function matches the type of Broadcast1ObserverType and the intellisense will reflect that.\n// If you want to change the signature you need only do so in the type Broadcast1ObserverType and the change will update the on and emit typings as well\n// here we want to reference \"this\" inside our observer function (preferred)\nobj.on.first(function (this: Timeline<any>, message: string) {\n    // we use \"this\", which will be the current timeline and the default log method to emit a logging event\n    this.log(message, 0);\n});\n\n// we don't need to reference \"this\" so we use arrow notation\nobj.on.first((message: string) => {\n    console.log(message);\n});\n

Similarily for second our observers would match Broadcast2Observer.

obj.on.second(function (this: Timeline<any>, value: number, value2: number) {\n    // we use \"this\", which will be the current timeline and the default log method to emit a logging event\n    this.log(`got value1: ${value} value2: ${value2}`, 0);\n});\n\nobj.on.second((value: number, value2: number) => {\n    console.log(`got value1: ${value} value2: ${value2}`);\n});\n
"},{"location":"core/moments/#existing-moment-factories","title":"Existing Moment Factories","text":"

You a already familiar with broadcast which passes the emited args to all subscribed observers, this section lists the existing built in moment factories:

"},{"location":"core/moments/#broadcast","title":"broadcast","text":"

Creates a moment that passes the emited args to all subscribed observers. Takes a single type parameter defining the observer signature and always returns void. Is not async.

import { broadcast } from \"@pnp/core\";\n\n// can have any method signature you want that returns void, \"this\" will always be set\ntype BroadcastObserver = (this: Timeline<any>, message: string) => void;\n\nconst moments = {\n    example: broadcast<BroadcastObserver>(),\n} as const;\n\nobj.on.example(function (this: Timeline<any>, message: string) {\n    this.log(message, 0);\n});\n\nobj.emit.example(\"Hello\");\n
"},{"location":"core/moments/#asyncreduce","title":"asyncReduce","text":"

Creates a moment that executes each observer asynchronously, awaiting the result and passes the returned arguments as the arguments to the next observer. This is very much like the redux pattern taking the arguments as the state which each observer may modify then returning a new state.

import { asyncReduce } from \"@pnp/core\";\n\n// can have any method signature you want, so long as it is async and returns a tuple matching in order the arguments, \"this\" will always be set\ntype AsyncReduceObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<[string, number]>;\n\nconst moments = {\n    example: asyncReduce<AsyncReduceObserver>(),\n} as const;\n\nobj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {\n\n    this.log(message, 0);\n\n    // we can manipulate the values\n    arg2++;\n\n    // always return a tuple of the passed arguments, possibly modified\n    return [arg1, arg2];\n});\n\nobj.emit.example(\"Hello\", 42);\n
"},{"location":"core/moments/#request","title":"request","text":"

Creates a moment where the first registered observer is used to asynchronously execute a request, returning a single result. If no result is returned (undefined) no further action is taken and the result will be undefined (i.e. additional observers are not used).

This is used by us to execute web requets, but would also serve to represent any async request such as a database read, file read, or provisioning step.

import { request } from \"@pnp/core\";\n\n// can have any method signature you want, \"this\" will always be set\ntype RequestObserver = (this: Timeline<any>, arg1: string, arg2: number) => Promise<string>;\n\nconst moments = {\n    example: request<RequestObserver>(),\n} as const;\n\nobj.on.example(async function (this: Timeline<any>, arg1: string, arg2: number) {\n\n    this.log(`Sending request: ${arg1}`, 0);\n\n    // request expects a single value result\n    return `result value ${arg2}`;\n});\n\nobj.emit.example(\"Hello\", 42);\n
"},{"location":"core/moments/#additional-examples","title":"Additional Examples","text":""},{"location":"core/moments/#waitall","title":"waitall","text":"

Perhaps you have a situation where you would like to wait until all of the subscribed observers for a given moment complete, but they can run async in parallel.

export function waitall<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<void> {\n\n    // this is the actual moment implementation, called each time a given moment occurs in the timeline\n    return function (observers: T[], ...args: any[]): void {\n\n        // we make a local ref of the observers\n        const obs = [...observers];\n\n        const promises = [];\n\n        // we loop through sending the args to each observer\n        for (let i = 0; i < obs.length; i++) {\n\n            // note that within every moment and observer \"this\" will be the current timeline object\n            promises.push(Reflect.apply(obs[i], this, args));\n        }\n\n        return Promise.all(promises).then(() => void(0));\n    };\n}\n
"},{"location":"core/moments/#first","title":"first","text":"

Perhaps you would instead like to only get the result of the first observer to return.

export function first<T extends ObserverFunction>(): (observers: T[], ...args: any[]) => Promise<any> {\n\n    // this is the actual moment implementation, called each time a given moment occurs in the timeline\n    return function (observers: T[], ...args: any[]): void {\n\n        // we make a local ref of the observers\n        const obs = [...observers];\n\n        const promises = [];\n\n        // we loop through sending the args to each observer\n        for (let i = 0; i < obs.length; i++) {\n\n            // note that within every moment and observer \"this\" will be the current timeline object\n            promises.push(Reflect.apply(obs[i], this, args));\n        }\n\n        return Promise.race(promises);\n    };\n}\n
"},{"location":"core/observers/","title":"@pnp/core : observers","text":"

Observers are used to implement all of the functionality within a Timeline's moments. Each moment defines the signature of observers you can register, and calling the observers is orchestrated by the implementation of the moment. A few facts about observers:

  • All observers are functions
  • The \"this\" of an observer is always the Timeline implementation that emitted the moment
  • Do not handle non-recoverable errors in observers, let them throw and they will be handled by the library appropriately and routed to the error moment.

For details on implementing observers for Queryable, please see this article.

"},{"location":"core/observers/#observer-inheritance","title":"Observer Inheritance","text":"

Timelines created from other timelines (i.e. how sp and graph libraries work) inherit all of the observers from the parent. Observers added to the parent will apply for all children.

When you make a change to the set of observers through any of the subscription methods outlined below that inheritance is broken. Meaning changes to the parent will no longer apply to that child, and changes to a child never affect a parent. This applies to ALL moments on change of ANY moment, there is no per-moment inheritance concept.

const sp = new spfi().using(...lots of behaviors);\n\n// web is current inheriting all observers from \"sp\"\nconst web = sp.web;\n\n// at this point web no longer inherits from \"sp\" and has its own observers\n// but still includes everything that was registered in sp before this call\nweb.on.log(...);\n\n// web2 inherits from sp as each invocation of .web creates a fresh IWeb instance\nconst web2 = sp.web;\n\n// list inherits from web's observers and will contain the extra `log` observer added above\nconst list = web.lists.getById(\"\");\n\n// this new behavior will apply to web2 and any subsequent objects created from sp\nsp.using(AnotherBehavior());\n\n// web will again inherit from sp through web2, the extra log handler is gone\n// list now ALSO is reinheriting from sp as it was pointing to web\nweb.using(AssignFrom(web2));\n// see below for more information on AssignFrom\n
"},{"location":"core/observers/#obserever-subscriptions","title":"Obserever Subscriptions","text":"

All timeline moments are exposed through the on property with three options for subscription.

"},{"location":"core/observers/#append","title":"Append","text":"

This is the default, and adds your observer to the end of the array of subscribed observers.

obj.on.log(function(this: Queryable, message: string, level: number) {\n    if (level > 1) {\n        console.log(message);\n    }\n});\n
"},{"location":"core/observers/#prepend","title":"Prepend","text":"

Using prepend will place your observer as the first item in the array of subscribed observers. There is no gaurantee it will always remain first, other code can also use prepend.

obj.on.log.prepend(function(this: Queryable, message: string, level: number) {\n    if (level > 1) {\n        console.log(message);\n    }\n});\n
"},{"location":"core/observers/#replace","title":"Replace","text":"

Replace will remove all other subscribed observers from a moment and add the supplied observer as the only one in the array of subscribed observers.

obj.on.log.replace(function(this: Queryable, message: string, level: number) {\n    if (level > 1) {\n        console.log(message);\n    }\n});\n
"},{"location":"core/observers/#toarray","title":"ToArray","text":"

The ToArray method creates a cloned copy of the array of registered observers for a given moment. Note that because it is a clone changes to the returned array do not affect the registered observers.

const arr = obj.on.log.toArray();\n
"},{"location":"core/observers/#clear","title":"Clear","text":"

This clears ALL observers for a given moment, returning true if any observers were removed, and false if no changes were made.

const didChange = obj.on.log.clear();\n
"},{"location":"core/observers/#special-behaviors","title":"Special Behaviors","text":"

The core library includes two special behaviors used to help manage observer inheritance. The best case is to manage inheritance using the methods described above, but these provide quick shorthand to help in certain scenarios. These are AssignFrom and CopyFrom.

"},{"location":"core/storage/","title":"@pnp/core : storage","text":"

This module provides a thin wrapper over the browser local and session storage. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.

"},{"location":"core/storage/#pnpclientstorage","title":"PnPClientStorage","text":"

The main export of this module, contains properties representing local and session storage.

import { PnPClientStorage } from \"@pnp/core\";\n\nconst storage = new PnPClientStorage();\nconst myvalue = storage.local.get(\"mykey\");\n
"},{"location":"core/storage/#pnpclientstoragewrapper","title":"PnPClientStorageWrapper","text":"

Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.

import { PnPClientStorage } from \"@pnp/core\";\n\nconst storage = new PnPClientStorage();\n\n// get a value from storage\nconst value = storage.local.get(\"mykey\");\n\n// put a value into storage\nstorage.local.put(\"mykey2\", \"my value\");\n\n// put a value into storage with an expiration\nstorage.local.put(\"mykey2\", \"my value\", new Date());\n\n// put a simple object into storage\n// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects\nstorage.local.put(\"mykey3\", {\n    key: \"value\",\n    key2: \"value2\",\n});\n\n// remove a value from storage\nstorage.local.delete(\"mykey3\");\n\n// get an item or add it if it does not exist\n// returns a promise in case you need time to get the value for storage\n// optionally takes a third parameter specifying the expiration\nstorage.local.getOrPut(\"mykey4\", () => {\n    return Promise.resolve(\"value\");\n});\n\n// delete expired items\nstorage.local.deleteExpired();\n
"},{"location":"core/storage/#cache-expiration","title":"Cache Expiration","text":"

The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished by explicitly calling the deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.

import { PnPClientStorage } from \"@pnp/core\";\n\nconst storage = new PnPClientStorage();\n\n// session storage\nstorage.session.deleteExpired();\n\n// local storage\nstorage.local.deleteExpired();\n\n// this returns a promise, so you can perform some activity after the expired items are removed:\nstorage.local.deleteExpired().then(_ => {\n    // init my application\n});\n

In previous versions we included code to automatically remove expired items. Due to a lack of necessity we removed that, but you can recreate the concept as shown below:

function expirer(timeout = 3000) {\n\n    // session storage\n    storage.session.deleteExpired();\n\n    // local storage\n    storage.local.deleteExpired();\n\n    setTimeout(() => expirer(timeout), timeout);\n}\n
"},{"location":"core/timeline/","title":"@pnp/core : timeline","text":"

Timeline provides base functionality for ochestrating async operations. A timeline defines a set of moments to which observers can be registered. Observers are functions that can act independently or together during a moment in the timeline. The model is event-like but each moment's implementation can be unique in how it interacts with the registered observers. Keep reading under Define Moments to understand more about what a moment is and how to create one.

The easiest way to understand Timeline is to walk through implementing a simple one below. You also review Queryable to see how we use Timeline internally to the library.

"},{"location":"core/timeline/#create-a-timeline","title":"Create a Timeline","text":"

Implementing a timeline involves several steps, each explained below.

  1. Define Moments
  2. Implement concrete Timeline class
"},{"location":"core/timeline/#define-moments","title":"Define Moments","text":"

A timeline is made up of a set of moments which are themselves defined by a plain object with one or more properties, each of which is a function. You can use predefined moments, or create your own to meet your exact requirements. Below we define two moments within the MyMoments object, first and second. These names are entirely your choice and the order moments are defined in the plain object carries no meaning.

The first moment uses a pre-defined moment implementation asyncReduce. This moment allows you to define a state based on the arguments of the observer function, in this case FirstObserver. asyncReduce takes those arguments, does some processing, and returns a promise resolving an array matching the input arguments in order and type with optionally changed values. Those values become the arguments to the next observer registered to that moment.

import { asyncReduce, ObserverAction, Timeline } from \"@pnp/core\";\n\n// the first observer is a function taking a number and async returning a number in an array\n// all asyncReduce observers must follow this pattern of returning async a tuple matching the args\nexport type FirstObserver = (this: any, counter: number) => Promise<[number]>;\n\n// the second observer is a function taking a number and returning void\nexport type SecondObserver = (this: any, result: number) => void;\n\n// this is a custom moment definition as an example.\nexport function report<T extends ObserverAction>(): (observers: T[], ...args: any[]) => void {\n\n    return function (observers: T[], ...args: any[]): void {\n\n        const obs = [...observers];\n\n        // for this \n        if (obs.length > 0) {\n             Reflect.apply(obs[0], this, args);\n        }\n    };\n}\n\n// this plain object defines the moments which will be available in our timeline\n// the property name \"first\" and \"second\" will be the moment names, used when we make calls such as instance.on.first and instance.on.second\nconst TestingMoments = {\n    first: asyncReduce<FirstObserver>(),\n    second: report<SecondObserver>(),\n} as const;\n// note as well the use of as const, this allows TypeScript to properly resolve all the complex typings and not treat the plain object as \"any\"\n
"},{"location":"core/timeline/#subclass-timeline","title":"Subclass Timeline","text":"

After defining our moments we need to subclass Timeline to define how those moments emit through the lifecycle of the Timeline. Timeline has a single abstract method \"execute\" you must implement. You will also need to provide a way for callers to trigger the protected \"start\" method.

// our implementation of timeline, note we use `typeof TestingMoments` and ALSO pass the testing moments object to super() in the constructor\nclass TestTimeline extends Timeline<typeof TestingMoments> {\n\n    // we create two unique refs for our implementation we will use\n    // to resolve the execute promise\n    private InternalResolveEvent = Symbol.for(\"Resolve\");\n    private InternalRejectEvent = Symbol.for(\"Reject\");\n\n    constructor() {\n        // we need to pass the moments to the base Timeline\n        super(TestingMoments);\n    }\n\n    // we implement the execute the method to define when, in what order, and how our moments are called. This give you full control within the Timeline framework\n    // to determine your implementation's behavior\n    protected async execute(init?: any): Promise<any> {\n\n        // we can always emit log to any subscribers\n        this.log(\"Starting\", 0);\n\n        // set our timeline to start in the next tick\n        setTimeout(async () => {\n\n            try {\n\n                // we emit our \"first\" event\n                let [value] = await this.emit.first(init);\n\n                // we emit our \"second\" event\n                [value] = await this.emit.second(value);\n\n                // we reolve the execute promise with the final value\n                this.emit[this.InternalResolveEvent](value);\n\n            } catch (e) {\n\n                // we emit our reject event\n                this.emit[this.InternalRejectEvent](e);\n                // we emit error to any subscribed observers\n                this.error(e);\n            }\n        }, 0);\n\n        // return a promise which we will resolve/reject during the timeline lifecycle\n        return new Promise((resolve, reject) => {\n            this.on[this.InternalResolveEvent].replace(resolve);\n            this.on[this.InternalRejectEvent].replace(reject);\n        });\n    }\n\n    // provide a method to trigger our timeline, this could be protected or called directly by the user, your choice\n    public go(startValue = 0): Promise<number> {\n\n        // here we take a starting number\n        return this.start(startValue);\n    }\n}\n
"},{"location":"core/timeline/#using-your-timeline","title":"Using your Timeline","text":"
import { TestTimeline } from \"./file.js\";\n\nconst tl = new TestTimeline();\n\n// register observer\ntl.on.first(async (n) => [++n]);\n\n// register observer\ntl.on.second(async (n) => [++n]);\n\n// h === 2\nconst h = await tl.go(0);\n\n// h === 7\nconst h2 = await tl.go(5);\n
"},{"location":"core/timeline/#understanding-the-timeline-lifecycle","title":"Understanding the Timeline Lifecycle","text":"

Now that you implemented a simple timeline let's take a minute to understand the lifecycle of a timeline execution. There are four moments always defined for every timeline: init, dispose, log, and error. Of these init and dispose are used within the lifecycle, while log and error are used as you need.

"},{"location":"core/timeline/#timeline-lifecycle","title":"Timeline Lifecycle","text":"
  • .on.init (always)
  • your moments as defined in execute, in our example:
  • .on.first
  • .on.second
  • .on.dispose (always)

As well the moments log and error exist on every Timeline derived class and can occur at any point during the lifecycle.

"},{"location":"core/timeline/#observer-inheritance","title":"Observer Inheritance","text":"

Let's say that you want to contruct a system whereby you can create Timeline based instances from other Timeline based instances - which is what Queryable does. Imagine we have a class with a pseudo-signature like:

class ExampleTimeline extends Timeline<typeof SomeMoments> {\n\n    // we create two unique refs for our implementation we will use\n    // to resolve the execute promise\n    private InternalResolveEvent = Symbol.for(\"Resolve\");\n    private InternalRejectEvent = Symbol.for(\"Reject\");\n\n    constructor(base: ATimeline) {\n\n        // we need to pass the moments to the base Timeline\n        super(TestingMoments, base.observers);\n    }\n\n    //...\n}\n

We can then use it like:

const tl1 = new ExampleTimeline();\ntl1.on.first(async (n) => [++n]);\ntl1.on.second(async (n) => [++n]);\n\n// at this point tl2's observer collection is a pointer to the same collection as tl1\nconst tl2 = new ExampleTimeline(tl1);\n\n// we add a second observer to first, it is applied to BOTH tl1 and tl2\ntl1.on.first(async (n) => [++n]);\n\n// BUT when we modify tl2's observers, either by adding or clearing a moment it begins to track its own collection\ntl2.on.first(async (n) => [++n]);\n
"},{"location":"core/util/","title":"@pnp/core : util","text":"

This module contains utility methods that you can import individually from the core library.

"},{"location":"core/util/#combine","title":"combine","text":"

Combines any number of paths, normalizing the slashes as required

import { combine } from \"@pnp/core\";\n\n// \"https://microsoft.com/something/more\"\nconst paths = combine(\"https://microsoft.com\", \"something\", \"more\");\n\n// \"also/works/with/relative\"\nconst paths2 = combine(\"/also/\", \"/works\", \"with/\", \"/relative\\\\\");\n
"},{"location":"core/util/#dateadd","title":"dateAdd","text":"

Manipulates a date, please see the Stack Overflow discussion from which this method was taken.

import { dateAdd } from \"@pnp/core\";\n\nconst now = new Date();\n\nconst newData = dateAdd(now, \"minute\", 10);\n
"},{"location":"core/util/#getguid","title":"getGUID","text":"

Creates a random guid, please see the Stack Overflow discussion from which this method was taken.

import { getGUID } from \"@pnp/core\";\n\nconst newGUID = getGUID();\n
"},{"location":"core/util/#getrandomstring","title":"getRandomString","text":"

Gets a random string containing the number of characters specified.

import { getRandomString } from \"@pnp/core\";\n\nconst randomString = getRandomString(10);\n
"},{"location":"core/util/#hop","title":"hOP","text":"

Shortcut for Object.hasOwnProperty. Determines if an object has a specified property.

import { HttpRequestError } from \"@pnp/queryable\";\nimport { hOP } from \"@pnp/core\";\n\nexport async function handleError(e: Error | HttpRequestError): Promise<void> {\n\n  //Checks to see if the error object has a property called isHttpRequestError. Returns a bool.\n  if (hOP(e, \"isHttpRequestError\")) {\n      // Handle this type or error\n  } else {\n    // not an HttpRequestError so we do something else\n\n  }\n}\n
"},{"location":"core/util/#jss","title":"jsS","text":"

Shorthand for JSON.stringify

import { jsS } from \"@pnp/core\";\n\nconst s: string = jsS({ hello: \"world\" });\n
"},{"location":"core/util/#isarray","title":"isArray","text":"

Determines if a supplied variable represents an array.

import { isArray } from \"@pnp/core\";\n\nconst x = [1, 2, 3];\n\nif (isArray(x)){\n    console.log(\"I am an array\");\n} else {\n    console.log(\"I am not an array\");\n}\n
"},{"location":"core/util/#isfunc","title":"isFunc","text":"

Determines if a supplied variable represents a function.

import { isFunc } from \"@pnp/core\";\n\npublic testFunction() {\n    console.log(\"test function\");\n    return\n}\n\nif (isFunc(testFunction)){\n    console.log(\"this is a function\");\n    testFunction();\n}\n
"},{"location":"core/util/#isurlabsolute","title":"isUrlAbsolute","text":"

Determines if a supplied url is absolute, returning true; otherwise returns false.

import { isUrlAbsolute } from \"@pnp/core\";\n\nconst webPath = 'https://{tenant}.sharepoint.com/sites/dev/';\n\nif (isUrlAbsolute(webPath)){\n    console.log(\"URL is absolute\");\n}else{\n    console.log(\"URL is not absolute\");\n}\n
"},{"location":"core/util/#objectdefinednotnull","title":"objectDefinedNotNull","text":"

Determines if an object is defined and not null.

import { objectDefinedNotNull } from \"@pnp/core\";\n\nconst obj = {\n    prop: 1\n};\n\nif (objectDefinedNotNull(obj)){\n    console.log(\"Not null\");\n} else {\n    console.log(\"Null\");\n}\n
"},{"location":"core/util/#stringisnullorempty","title":"stringIsNullOrEmpty","text":"

Determines if a supplied string is null or empty.

import { stringIsNullOrEmpty } from \"@pnp/core\";\n\nconst x: string = \"hello\";\n\nif (stringIsNullOrEmpty(x)){\n    console.log(\"Null or empty\");\n} else {\n    console.log(\"Not null or empty\");\n}\n
"},{"location":"core/util/#gethashcode","title":"getHashCode","text":"

Gets a (mostly) unique hashcode for a specified string.

Taken from: https://stackoverflow.com/questions/6122571/simple-non-secure-hash-function-for-javascript

import { getHashCode } from \"@pnp/core\";\n\nconst x: string = \"hello\";\n\nconst hash = getHashCode(x);\n
"},{"location":"core/util/#delay","title":"delay","text":"

Provides an awaitable delay specified in milliseconds.

import { delay } from \"@pnp/core\";\n\n// wait 1 second\nawait delay(1000);\n\n// wait 10 second\nawait delay(10000);\n
"},{"location":"graph/behaviors/","title":"@pnp/graph : behaviors","text":"

The article describes the behaviors exported by the @pnp/graph library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/nodejs.

"},{"location":"graph/behaviors/#defaultinit","title":"DefaultInit","text":"

The DefaultInit behavior, itself a composed behavior includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit and ensures the request url is absolute.

import { graphfi, DefaultInit } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultInit());\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#defaultheaders","title":"DefaultHeaders","text":"

The DefaultHeaders behavior uses InjectHeaders to set the Content-Type header.

import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nawait graph.users();\n

DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.

"},{"location":"graph/behaviors/#paged","title":"Paged","text":"

Added in 3.4.0

The Paged behavior allows you to access the information in a collection through a series of pages. While you can use it directly, you will likely use the paged method of the collections which handles things for you.

Note that not all entity types support count and where it is unsupported it will return 0.

Basic example, read all users:

import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nconst allUsers = [];\nlet users = await graph.users.top(300).paged();\n\nallUsers.push(...users.value);\n\nwhile (users.hasNext) {\n  users = await users.next();\n  allUsers.push(...users.value);\n}\n\nconsole.log(`All users: ${JSON.stringify(allUsers)}`);\n

Beyond the basics other query operations are supported such as filter and select.

import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nconst allUsers = [];\nlet users = await graph.users.top(50).select(\"userPrincipalName\", \"displayName\").filter(\"startswith(displayName, 'A')\").paged();\n\nallUsers.push(...users.value);\n\nwhile (users.hasNext) {\n  users = await users.next();\n  allUsers.push(...users.value);\n}\n\nconsole.log(`All users: ${JSON.stringify(allUsers)}`);\n

And similarly for groups, showing the same pattern for different types of collections

import { graphfi, DefaultHeaders } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi().using(DefaultHeaders());\n\nconst allGroups = [];\nlet groups = await graph.groups.paged();\n\nallGroups.push(...groups.value);\n\nwhile (groups.hasNext) {\n  groups = await groups.next();\n  allGroups.push(...groups.value);\n}\n\nconsole.log(`All groups: ${JSON.stringify(allGroups)}`);\n
"},{"location":"graph/behaviors/#endpoint","title":"Endpoint","text":"

This behavior is used to change the endpoint to which requests are made, either \"beta\" or \"v1.0\". This allows you to easily switch back and forth between the endpoints as needed.

import { graphfi, Endpoint } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst beta = graphfi().using(Endpoint(\"beta\"));\n\nconst vOne = graphfi().using(Endpoint(\"v1.0\"));\n\nawait beta.users();\n\nawait vOne.users();\n

It can also be used at any point in the fluid chain to switch an isolated request to a different endpoint.

import { graphfi, Endpoint } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// will point to v1 by default\nconst graph = graphfi().using();\n\nconst user = graph.users.getById(\"{id}\");\n\n// this only applies to the \"user\" instance now\nconst userInfoFromBeta = user.using(Endpoint(\"beta\"))();\n

Finally, if you always want to make your requests to the beta end point (as an example) it is more efficient to set it in the graphfi factory.

import { graphfi } from \"@pnp/graph\";\n\nconst beta = graphfi(\"https://graph.microsoft.com/beta\");\n
"},{"location":"graph/behaviors/#graphbrowser","title":"GraphBrowser","text":"

A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.

The baseUrl prop can be used to configure the graph endpoint to which requests will be sent.

If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.

import { graphfi, GraphBrowser } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphBrowser());\n\nawait graph.users();\n

You can also set a baseUrl. This is equivelent to calling graphfi with an absolute url.

import { graphfi, GraphBrowser } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(GraphBrowser({ baseUrl: \"https://graph.microsoft.com/v1.0\" }));\n\n// this is the same as the above, and maybe a litter easier to read, and is more efficient\n// const graph = graphfi(\"https://graph.microsoft.com/v1.0\").using(GraphBrowser());\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#spfx","title":"SPFx","text":"

This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, and DefaultParse. It also replaces any authentication present with a method to get a token from the SPFx aadTokenProviderFactory.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst graph = graphfi(...).using(SPFx(this.context));\n\nawait graph.users();\n

Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.

import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'\nimport { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'\n\nconst sp = spfi().using(spSPFx(this.context));\nconst graph = graphfi().using(graphSPFx(this.context));\n

If you want to use a different form of authentication you can apply that behavior after SPFx to override it. In this case we are using the client MSAL authentication.

"},{"location":"graph/behaviors/#spfxtoken","title":"SPFxToken","text":"

Added in 3.12

Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst graph = graphfi(...).using(SPFxToken(this.context));\n\nawait graph.users();\n
import { graphfi } from \"@pnp/graph\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport \"@pnp/graph/users\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst graph = graphfi().using(SPFx(this.context), MSAL({ /* proper MSAL settings */}));\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#telemetry","title":"Telemetry","text":"

This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.

You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.

import { graphfi, Telemetry } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(Telemetry());\n\nawait graph.users();\n
"},{"location":"graph/behaviors/#consistencylevel","title":"ConsistencyLevel","text":"

Using this behavior you can set the consistency level of your requests. You likely won't need to use this directly as we include it where needed.

Basic usage:

import { graphfi, ConsistencyLevel } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(ConsistencyLevel());\n\nawait graph.users();\n

If in the future there is another value other than \"eventual\" you can supply it to the behavior. For now only \"eventual\" is a valid value, which is the default, so you do not need to pass it as a param.

import { graphfi, ConsistencyLevel } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi().using(ConsistencyLevel(\"{level value}\"));\n\nawait graph.users();\n
"},{"location":"graph/bookings/","title":"@pnp/graph/bookings","text":"

Represents the Bookings services available to a user.

You can learn more by reading the Official Microsoft Graph Documentation.

"},{"location":"graph/bookings/#ibookingcurrencies-ibookingcurrency-ibookingbusinesses-ibookingbusiness-ibookingappointments-ibookingappointment-ibookingcustomers-ibookingcustomer-ibookingservices-ibookingservice-ibookingstaffmembers-ibookingstaffmember-ibookingcustomquestions-ibookingcustomquestion","title":"IBookingCurrencies, IBookingCurrency, IBookingBusinesses, IBookingBusiness, IBookingAppointments, IBookingAppointment, IBookingCustomers, IBookingCustomer, IBookingServices, IBookingService, IBookingStaffMembers, IBookingStaffMember, IBookingCustomQuestions, IBookingCustomQuestion","text":""},{"location":"graph/bookings/#get-booking-currencies","title":"Get Booking Currencies","text":"

Get the supported currencies

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\n\nconst graph = graphfi(...);\n\n// Get all the currencies\nconst currencies = await graph.bookingCurrencies();\n// get the details of the first currency\nconst currency = await graph.bookingCurrencies.getById(currencies[0].id)();\n
"},{"location":"graph/bookings/#work-with-booking-businesses","title":"Work with Booking Businesses","text":"

Get the bookings businesses

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\n\nconst graph = graphfi(...);\n\n// Get all the businesses\nconst businesses = await graph.bookingBusinesses();\n// get the details of the first business\nconst business = graph.bookingBusinesses.getById(businesses[0].id)();\nconst businessDetails = await business();\n// get the business calendar\nconst calView = await business.calendarView(\"2022-06-01\", \"2022-08-01\")();\n// publish the business\nawait business.publish();\n// unpublish the business\nawait business.unpublish();\n
"},{"location":"graph/bookings/#work-with-booking-services","title":"Work with Booking Services","text":"

Get the bookings business services

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingService } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business services\nconst services = await business.services();\n// add a service\nconst newServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};\nconst newService = services.add(newServiceDesc);\n// get service by id\nconst service = await business.services.getById({service id})();\n// update service\nconst updateServiceDesc: BookingService = {booking service details -- see Microsoft Graph documentation};\nconst update = await business.services.getById({service id}).update(updateServiceDesc);\n// delete service\nawait business.services.getById({service id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-customers","title":"Work with Booking Customers","text":"

Get the bookings business customers

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingCustomer } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business customers\nconst customers = await business.customers();\n// add a customer\nconst newCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};\nconst newCustomer = customers.add(newCustomerDesc);\n// get customer by id\nconst customer = await business.customers.getById({customer id})();\n// update customer\nconst updateCustomerDesc: BookingCustomer = {booking customer details -- see Microsoft Graph documentation};\nconst update = await business.customers.getById({customer id}).update(updateCustomerDesc);\n// delete customer\nawait business.customers.getById({customer id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-staffmembers","title":"Work with Booking StaffMembers","text":"

Get the bookings business staffmembers

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingStaffMember } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business staff members\nconst staffmembers = await business.staffMembers();\n// add a staff member\nconst newStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};\nconst newStaffMember = staffmembers.add(newStaffMemberDesc);\n// get staff member by id\nconst staffmember = await business.staffMembers.getById({staff member id})();\n// update staff member\nconst updateStaffMemberDesc: BookingStaffMember = {booking staff member details -- see Microsoft Graph documentation};\nconst update = await business.staffMembers.getById({staff member id}).update(updateStaffMemberDesc);\n// delete staffmember\nawait business.staffMembers.getById({staff member id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-appointments","title":"Work with Booking Appointments","text":"

Get the bookings business appointments

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingAppointment } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business appointments\nconst appointments = await business.appointments();\n// add a appointment\nconst newAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};\nconst newAppointment = appointments.add(newAppointmentDesc);\n// get appointment by id\nconst appointment = await business.appointments.getById({appointment id})();\n// cancel the appointment\nawait appointment.cancel();\n// update appointment\nconst updateAppointmentDesc: BookingAppointment = {booking appointment details -- see Microsoft Graph documentation};\nconst update = await business.appointments.getById({appointment id}).update(updateAppointmentDesc);\n// delete appointment\nawait business.appointments.getById({appointment id}).delete();\n
"},{"location":"graph/bookings/#work-with-booking-custom-questions","title":"Work with Booking Custom Questions","text":"

Get the bookings business custom questions

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/bookings\";\nimport { BookingCustomQuestion } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst business = graph.bookingBusinesses.getById({Booking Business Id})();\n// get the business custom questions\nconst customQuestions = await business.customQuestions();\n// add a custom question\nconst newCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};\nconst newCustomQuestion = customQuestions.add(newCustomQuestionDesc);\n// get custom question by id\nconst customquestion = await business.customQuestions.getById({customquestion id})();\n// update custom question\nconst updateCustomQuestionDesc: BookingCustomQuestion = {booking custom question details -- see Microsoft Graph documentation};\nconst update = await business.customQuestions.getById({custom question id}).update(updateCustomQuestionDesc);\n// delete custom question\nawait business.customQuestions.getById({customquestion id}).delete();\n
"},{"location":"graph/calendars/","title":"@pnp/graph/calendars","text":"

More information can be found in the official Graph documentation:

  • Calendar Resource Type
  • Event Resource Type
"},{"location":"graph/calendars/#icalendar-icalendars","title":"ICalendar, ICalendars","text":""},{"location":"graph/calendars/#get-all-calendars-for-a-user","title":"Get All Calendars For a User","text":"
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars();\n\nconst myCalendars = await graph.me.calendars();\n\n
"},{"location":"graph/calendars/#get-a-specific-calendar-for-a-user","title":"Get a Specific Calendar For a User","text":"
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';\n\nconst calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)();\n\nconst myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();\n
"},{"location":"graph/calendars/#get-a-users-default-calendar","title":"Get a User's Default Calendar","text":"
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar();\n\nconst myCalendar = await graph.me.calendar();\n
"},{"location":"graph/calendars/#get-events-for-a-users-default-calendar","title":"Get Events For a User's Default Calendar","text":"
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\n// You can get the default calendar events\nconst events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events();\n// or get all events for the user\nconst events = await graph.users.getById('user@tenant.onmicrosoft.com').events();\n\n// You can get my default calendar events\nconst events = await graph.me.calendar.events();\n// or get all events for me\nconst events = await graph.me.events();\n
"},{"location":"graph/calendars/#get-events-by-id","title":"Get Events By ID","text":"

You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar.

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA==';\n\nconst EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';\n\n// Get events by ID\nconst event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID);\n\nconst events = await graph.me.events.getByID(EventID);\n\n// Get an event by ID from a specific calendar\nconst event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID);\n\nconst events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);\n\n
"},{"location":"graph/calendars/#create-events","title":"Create Events","text":"

This will work on any IEvents objects (e.g. anything accessed using an events key).

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nawait graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add(\n{\n  \"subject\": \"Let's go for lunch\",\n  \"body\": {\n    \"contentType\": \"HTML\",\n    \"content\": \"Does late morning work for you?\"\n  },\n  \"start\": {\n      \"dateTime\": \"2017-04-15T12:00:00\",\n      \"timeZone\": \"Pacific Standard Time\"\n  },\n  \"end\": {\n      \"dateTime\": \"2017-04-15T14:00:00\",\n      \"timeZone\": \"Pacific Standard Time\"\n  },\n  \"location\":{\n      \"displayName\":\"Harry's Bar\"\n  },\n  \"attendees\": [\n    {\n      \"emailAddress\": {\n        \"address\":\"samanthab@contoso.onmicrosoft.com\",\n        \"name\": \"Samantha Booth\"\n      },\n      \"type\": \"required\"\n    }\n  ]\n});\n
"},{"location":"graph/calendars/#update-events","title":"Update Events","text":"

This will work on any IEvents objects (e.g. anything accessed using an events key).

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';\n\nawait graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({\n    reminderMinutesBeforeStart: 99,\n});\n
"},{"location":"graph/calendars/#delete-event","title":"Delete Event","text":"

This will work on any IEvents objects (e.g. anything accessed using an events key).

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\nconst EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';\n\nawait graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete();\n\nawait graph.me.events.getById(EVENT_ID).delete();\n
"},{"location":"graph/calendars/#get-calendar-for-a-group","title":"Get Calendar for a Group","text":"
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/groups';\n\nconst graph = graph.using(SPFx(this.context));\n\nconst calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();\n
"},{"location":"graph/calendars/#get-events-for-a-group","title":"Get Events for a Group","text":"
import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/groups';\n\nconst graph = graphfi(...);\n\n// You can do one of\nconst events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events();\n// or\nconst events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();\n
"},{"location":"graph/calendars/#get-calendar-view","title":"Get Calendar View","text":"

Gets the events in a calendar during a specified date range.

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n\n// basic request, note need to invoke the returned queryable\nconst view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\")();\n\n// you can use select, top, etc to filter your returned results\nconst view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)();\n\n// you can specify times along with the dates\nconst view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")();\n\nconst view4 = await graph.me.calendarView(\"2020-01-01\", \"2020-03-01\")();\n
"},{"location":"graph/calendars/#find-rooms","title":"Find Rooms","text":"

Gets the emailAddress objects that represent all the meeting rooms in the user's tenant or in a specific room list.

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\n// basic request, note need to invoke the returned queryable\nconst rooms1 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms()();\n// you can pass a room list to filter results\nconst rooms2 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms('roomlist@tenant.onmicrosoft.com')();\n// you can use select, top, etc to filter your returned results\nconst rooms3 = await graph.users.getById('user@tenant.onmicrosoft.com').findRooms().select('name').top(10)();\n
"},{"location":"graph/calendars/#get-event-instances","title":"Get Event Instances","text":"

Get the instances (occurrences) of an event for a specified time range.

If the event is a seriesMaster type, this returns the occurrences and exceptions of the event in the specified time range.

import { graphfi } from \"@pnp/graph\";\nimport '@pnp/graph/calendars';\nimport '@pnp/graph/users';\n\nconst graph = graphfi(...);\nconst event = graph.me.events.getById('');\n// basic request, note need to invoke the returned queryable\nconst instances = await event.instances(\"2020-01-01\", \"2020-03-01\")();\n// you can use select, top, etc to filter your returned results\nconst instances2 = await event.instances(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)();\n// you can specify times along with the dates\nconst instance3 = await event.instances(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")(); \n
"},{"location":"graph/cloud-communications/","title":"@pnp/graph/cloud-communications","text":"

The ability to retrieve information about a user's presence, including their availability and user activity.

More information can be found in the official Graph documentation:

  • Presence Type
"},{"location":"graph/cloud-communications/#ipresence","title":"IPresence","text":""},{"location":"graph/cloud-communications/#get-users-presence","title":"Get users presence","text":"

Gets a list of all the contacts for the user.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/cloud-communications\";\n\nconst graph = graphfi(...);\n\nconst presenceMe = await graph.me.presence();\n\nconst presenceThem = await graph.users.getById(\"99999999-9999-9999-9999-999999999999\").presence();\n\n
"},{"location":"graph/cloud-communications/#get-presence-for-multiple-users","title":"Get presence for multiple users","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/cloud-communications\";\n\nconst graph = graphfi(...);\n\nconst presenceList = await graph.communications.getPresencesByUserId([\"99999999-9999-9999-9999-999999999999\"]);\n\n
"},{"location":"graph/columns/","title":"Graph Columns","text":"

More information can be found in the official Graph documentation:

  • Columns Resource Type
  • List Resource Type
  • Content Type Resource Type

"},{"location":"graph/columns/#get-columns","title":"Get Columns","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n//Needed for content types\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst siteColumns = await graph.site.getById(\"{site identifier}\").columns();\nconst listColumns = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns();\nconst contentTypeColumns = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").columns();\n
"},{"location":"graph/columns/#get-columns-by-id","title":"Get Columns by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n//Needed for content types\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst siteColumn = await graph.site.getById(\"{site identifier}\").columns.getById(\"{column identifier}\")();\nconst listColumn = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns.getById(\"{column identifier}\")();\nconst contentTypeColumn = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").columns.getById(\"{column identifier}\")();\n
"},{"location":"graph/columns/#add-a-columns-sites-and-list","title":"Add a Columns (Sites and List)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst sampleColumn: ColumnDefinition = {\n    description: \"PnPTestColumn Description\",\n    enforceUniqueValues: false,\n    hidden: false,\n    indexed: false,\n    name: \"PnPTestColumn\",\n    displayName: \"PnPTestColumn\",\n    text: {\n        allowMultipleLines: false,\n        appendChangesToExistingText: false,\n        linesForEditing: 0,\n        maxLength: 255,\n    },\n};\n\nconst siteColumn = await graph.site.getById(\"{site identifier}\").columns.add(sampleColumn);\nconst listColumn = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns.add(sampleColumn);\n
"},{"location":"graph/columns/#add-a-column-reference-content-types","title":"Add a Column Reference (Content Types)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for content types\nimport \"@pnp/graph/content-ypes\";\n\nconst graph = graphfi(...);\n\nconst siteColumn = await graph.site.getById(\"{site identifier}\").columns.getById(\"{column identifier}\")();\nconst contentTypeColumn = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").columns.addRef(siteColumn);\n
"},{"location":"graph/columns/#update-a-column-sites-and-list","title":"Update a Column (Sites and List)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst updatedSiteColumn = await site.columns.getById(\"{column identifier}\").update({ displayName: \"New Name\" });\nconst updateListColumn = await site.lists.getById(\"{list identifier}\").columns.getById(\"{column identifier}\").update({ displayName: \"New Name\" });\n
"},{"location":"graph/columns/#delete-a-column","title":"Delete a Column","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n//Needed for content types\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst siteColumn = await site.columns.getById(\"{column identifier}\").delete();\nconst listColumn = await site.lists.getById(\"{list identifier}\").columns.getById(\"{column identifier}\").delete();\nconst contentTypeColumn = await site.contentTypes.getById(\"{content type identifier}\").columns.getById(\"{column identifier}\").delete();\n
"},{"location":"graph/contacts/","title":"@pnp/graph/contacts","text":"

The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graphfi(). Through the methods described you can add and edit both contacts and folders in a users Outlook.

More information can be found in the official Graph documentation:

  • Contact Resource Type
"},{"location":"graph/contacts/#icontact-icontacts-icontactfolder-icontactfolders","title":"IContact, IContacts, IContactFolder, IContactFolders","text":""},{"location":"graph/contacts/#set-up-notes","title":"Set up notes","text":"

To make user calls you can use getById where the id is the users email address. Contact ID, Folder ID, and Parent Folder ID use the following format \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"

"},{"location":"graph/contacts/#get-all-of-the-contacts","title":"Get all of the Contacts","text":"

Gets a list of all the contacts for the user.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\"\nimport \"@pnp/graph/contacts\"\n\nconst graph = graphfi(...);\n\nconst contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts();\n\nconst contacts2 = await graph.me.contacts();\n\n
"},{"location":"graph/contacts/#get-contact-by-id","title":"Get Contact by Id","text":"

Gets a specific contact by ID for the user.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\";\n\nconst contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)();\n\nconst contact2 = await graph.me.contacts.getById(contactID)();\n\n
"},{"location":"graph/contacts/#add-a-new-contact","title":"Add a new Contact","text":"

Adds a new contact for the user.

import { graphfi } from \"@pnp/graph\";\nimport { EmailAddress } from \"@microsoft/microsoft-graph-types\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\nconst addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\n
"},{"location":"graph/contacts/#update-a-contact","title":"Update a Contact","text":"

Updates a specific contact by ID for teh designated user

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\";\n\nconst updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: \"1986-05-30\" });\n\nconst updContact2 = await graph.me.contacts.getById(contactID).update({birthday: \"1986-05-30\" });\n\n
"},{"location":"graph/contacts/#delete-a-contact","title":"Delete a Contact","text":"

Delete a contact from the list of contacts for a user.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\";\n\nconst delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete();\n\nconst delContact2 = await graph.me.contacts.getById(contactID).delete();\n\n
"},{"location":"graph/contacts/#get-all-of-the-contact-folders","title":"Get all of the Contact Folders","text":"

Get all the folders for the designated user's contacts

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders();\n\nconst contactFolders2 = await graph.me.contactFolders();\n\n
"},{"location":"graph/contacts/#get-contact-folder-by-id","title":"Get Contact Folder by Id","text":"

Get a contact folder by ID for the specified user

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)();\n\nconst contactFolder2 = await graph.me.contactFolders.getById(folderID)();\n\n
"},{"location":"graph/contacts/#add-a-new-contact-folder","title":"Add a new Contact Folder","text":"

Add a new folder in the users contacts

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst parentFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=\";\n\nconst addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add(\"New Folder\", parentFolderID);\n\nconst addedContactFolder2 = await graph.me.contactFolders.add(\"New Folder\", parentFolderID);\n\n
"},{"location":"graph/contacts/#update-a-contact-folder","title":"Update a Contact Folder","text":"

Update an existing folder in the users contacts

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: \"Updated Folder\" });\n\nconst updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: \"Updated Folder\" });\n\n
"},{"location":"graph/contacts/#delete-a-contact-folder","title":"Delete a Contact Folder","text":"

Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete();\n\nconst delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();\n\n
"},{"location":"graph/contacts/#get-all-of-the-contacts-from-the-contact-folder","title":"Get all of the Contacts from the Contact Folder","text":"

Get all the contacts in a folder

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts();\n\nconst contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();\n\n
"},{"location":"graph/contacts/#get-child-folders-of-the-contact-folder","title":"Get Child Folders of the Contact Folder","text":"

Get child folders from contact folder

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders();\n\nconst childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();\n\n
"},{"location":"graph/contacts/#add-a-new-child-folder","title":"Add a new Child Folder","text":"

Add a new child folder to a contact folder

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\n\nconst addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID);\n\nconst addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID);\n
"},{"location":"graph/contacts/#get-child-folder-by-id","title":"Get Child Folder by Id","text":"

Get child folder by ID from user contacts

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\nconst subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\";\n\nconst childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)();\n\nconst childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();\n
"},{"location":"graph/contacts/#add-contact-in-child-folder-of-contact-folder","title":"Add Contact in Child Folder of Contact Folder","text":"

Add a new contact to a child folder

import { graphfi } from \"@pnp/graph\";\nimport { EmailAddress } from \"./@microsoft/microsoft-graph-types\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/contacts\";\n\nconst graph = graphfi(...);\n\nconst folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\";\nconst subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\";\n\nconst addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\nconst addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);\n\n
"},{"location":"graph/content-types/","title":"Graph Content Types","text":"

More information can be found in the official Graph documentation:

  • Columns Resource Type
  • List Resource Type
  • Content Type Resource Type

"},{"location":"graph/content-types/#get-content-types","title":"Get Content Types","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteContentTypes = await graph.site.getById(\"{site identifier}\").contentTypes();\nconst listContentTypes = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes();\n
"},{"location":"graph/content-types/#get-content-types-by-id","title":"Get Content Types by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteContentType = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\")();\nconst listContentType = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getById(\"{content type identifier}\")();\n
"},{"location":"graph/content-types/#add-a-content-type-site","title":"Add a Content Type (Site)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst sampleContentType: ContentType = {\n    name: \"PnPTestContentType\",\n    description: \"PnPTestContentType Description\",\n    base: {\n        name: \"Item\",\n        id: \"0x01\",\n    },\n    group: \"PnPTest Content Types\",\n    id: \"0x0100CDB27E23CEF44850904C80BD666FA645\",\n};\n\nconst siteContentType = await graph.sites.getById(\"{site identifier}\").contentTypes.add(sampleContentType);\n
"},{"location":"graph/content-types/#add-a-content-type-copy-list","title":"Add a Content Type - Copy (List)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/lists\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\n//Get a list of compatible site content types for the list\nconst siteContentType = await graph.site.getById(\"{site identifier}\").getApplicableContentTypesForList(\"{list identifier}\")();\n//Get a specific content type from the site.\nconst siteContentType = await graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\")();\nconst listContentType = await graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.addCopy(siteContentType);\n
"},{"location":"graph/content-types/#update-a-content-type-sites-and-list","title":"Update a Content Type (Sites and List)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/columns\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst updatedSiteContentType = await site.contentTypes.getById(\"{content type identifier}\").update({ description: \"New Description\" });\nconst updateListContentType = await site.lists.getById(\"{list identifier}\").contentTypes.getById(\"{content type identifier}\").update({ description: \"New Description\" });\n
"},{"location":"graph/content-types/#delete-a-content-type","title":"Delete a Content Type","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nawait graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\").delete();\nawait graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getById(\"{content type identifier}\").delete();\n
"},{"location":"graph/content-types/#get-compatible-content-types-from-hub","title":"Get Compatible Content Types from Hub","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteContentTypes = await graph.site.getById(\"{site identifier}\").contentTypes.getCompatibleHubContentTypes();\nconst listContentTypes = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getCompatibleHubContentTypes();\n
"},{"location":"graph/content-types/#addsync-content-types-from-hub-site-and-list","title":"Add/Sync Content Types from Hub (Site and List)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n//Needed for lists\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst hubSiteContentTypes = await graph.site.getById(\"{site identifier}\").contentTypes.getCompatibleHubContentTypes();\nconst siteContentType = await graph.site.getById(\"{site identifier}\").contentTypes.addCopyFromContentTypeHub(hubSiteContentTypes[0].Id);\n\nconst hubListContentTypes = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.getCompatibleHubContentTypes();\nconst listContentType = await graph.site.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").contentTypes.addCopyFromContentTypeHub(hubListContentTypes[0].Id);\n
"},{"location":"graph/content-types/#site-content-type-ispublished-publish-unpublish","title":"Site Content Type (isPublished, Publish, Unpublish)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst siteContentType = graph.site.getById(\"{site identifier}\").contentTypes.getById(\"{content type identifier}\");\nconst isPublished = await siteContentType.isPublished();\nawait siteContentType.publish();\nawait siteContentType.unpublish();;\n
"},{"location":"graph/content-types/#associate-content-type-with-hub-sites","title":"Associate Content Type with Hub Sites","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\nconst hubSiteUrls: string[] = [hubSiteUrl1, hubSiteUrl2, hubSiteUrl3];\nconst propagateToExistingLists = true;\n// NOTE: the site must be the content type hub\nconst contentTypeHub = graph.site.getById(\"{content type hub site identifier}\");\nconst siteContentType = await contentTypeHub.contentTypes.getById(\"{content type identifier}\").associateWithHubSites(hubSiteUrls, propagateToExistingLists);\n
"},{"location":"graph/content-types/#copy-a-file-to-a-default-content-location-in-a-content-type","title":"Copy a file to a default content location in a content type","text":"

Not fully implemented, requires Files support

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/content-types\";\n\nconst graph = graphfi(...);\n\n// Not fully implemented\nconst sourceFile: ItemReference = {};\nconst destinationFileName: string = \"NewFileName\";\n\nconst site = graph.site.getById(\"{site identifier}\");\nconst siteContentType = await site.contentTypes.getById(\"{content type identifier}\").copyToDefaultContentLocation(sourceFile, destinationFileName);\n
"},{"location":"graph/directoryobjects/","title":"@pnp/graph/directoryObjects","text":"

Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types.

More information can be found in the official Graph documentation:

  • DirectoryObject Resource Type
"},{"location":"graph/directoryobjects/#idirectoryobject-idirectoryobjects","title":"IDirectoryObject, IDirectoryObjects","text":""},{"location":"graph/directoryobjects/#the-groups-and-directory-roles-for-the-user","title":"The groups and directory roles for the user","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf();\n\nconst memberOf2 = await graph.me.memberOf();\n\n
"},{"location":"graph/directoryobjects/#return-all-the-groups-the-user-group-or-directoryobject-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","title":"Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups();\n\nconst memberGroups2 = await graph.me.getMemberGroups();\n\n// Returns only security enabled groups\nconst memberGroups3 = await graph.me.getMemberGroups(true);\n\nconst memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();\n\n
"},{"location":"graph/directoryobjects/#returns-all-the-groups-administrative-units-and-directory-roles-that-a-user-group-or-directory-object-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","title":"Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects();\n\nconst memberObjects2 = await graph.me.getMemberObjects();\n\n// Returns only security enabled groups\nconst memberObjects3 = await graph.me.getMemberObjects(true);\n\nconst memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();\n
"},{"location":"graph/directoryobjects/#check-for-membership-in-a-specified-list-of-groups","title":"Check for membership in a specified list of groups","text":"

And returns from that list those groups of which the specified user, group, or directory object is a member

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);\n\nconst checkedMembers2 = await graph.me.checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);\n\nconst checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);\n
"},{"location":"graph/directoryobjects/#get-directoryobject-by-id","title":"Get directoryObject by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/directory-objects\";\n\nconst graph = graphfi(...);\n\nconst dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');\n\n
"},{"location":"graph/directoryobjects/#delete-directoryobject","title":"Delete directoryObject","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/directory-objects\";\n\nconst graph = graphfi(...);\n\nconst deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()\n\n
"},{"location":"graph/groups/","title":"@pnp/graph/groups","text":"

Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent.

Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups.

You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation.

"},{"location":"graph/groups/#igroup-igroups","title":"IGroup, IGroups","text":""},{"location":"graph/groups/#add-a-group","title":"Add a Group","text":"

Add a new group.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport { GroupType } from '@pnp/graph/groups';\n\nconst graph = graphfi(...);\n\nconst groupAddResult = await graph.groups.add(\"GroupName\", \"Mail_NickName\", GroupType.Office365);\nconst group = await groupAddResult.group();\n
"},{"location":"graph/groups/#delete-a-group","title":"Delete a Group","text":"

Deletes an existing group.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").delete();\n
"},{"location":"graph/groups/#update-group-properties","title":"Update Group Properties","text":"

Updates an existing group.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").update({ displayName: newName, propertyName: updatedValue});\n
"},{"location":"graph/groups/#add-favorite","title":"Add favorite","text":"

Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").addFavorite();\n
"},{"location":"graph/groups/#remove-favorite","title":"Remove favorite","text":"

Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").removeFavorite();\n
"},{"location":"graph/groups/#reset-unseen-count","title":"Reset Unseen Count","text":"

Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").resetUnseenCount();\n
"},{"location":"graph/groups/#subscribe-by-mail","title":"Subscribe By Mail","text":"

Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").subscribeByMail();\n
"},{"location":"graph/groups/#unsubscribe-by-mail","title":"Unsubscribe By Mail","text":"

Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").unsubscribeByMail();\n
"},{"location":"graph/groups/#get-calendar-view","title":"Get Calendar View","text":"

Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst startDate = new Date(\"2020-04-01\");\nconst endDate = new Date(\"2020-03-01\");\n\nconst events = graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").getCalendarView(startDate, endDate);\n
"},{"location":"graph/groups/#group-photo-operations","title":"Group Photo Operations","text":"

See Photos

"},{"location":"graph/groups/#group-membership","title":"Group Membership","text":"

Get the members and/or owners of a group.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/members\";\n\nconst graph = graphfi(...);\nconst members = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").members();\nconst owners = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").owners();\n
"},{"location":"graph/groups/#get-the-team-site-for-a-group","title":"Get the Team Site for a Group","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/sites/group\";\n\nconst graph = graphfi(...);\n\nconst teamSite = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").sites.root();\nconst url = teamSite.webUrl\n
"},{"location":"graph/insights/","title":"@pnp/graph/insights","text":"

This module helps you get Insights in form of Trending, Used and Shared. The results are based on relationships calculated using advanced analytics and machine learning techniques.

"},{"location":"graph/insights/#iinsights","title":"IInsights","text":""},{"location":"graph/insights/#get-all-trending-documents","title":"Get all Trending documents","text":"

Returns documents from OneDrive and SharePoint sites trending around a user.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst trending = await graph.me.insights.trending()\n\nconst trending = await graph.users.getById(\"userId\").insights.trending()\n
"},{"location":"graph/insights/#get-a-trending-document-by-id","title":"Get a Trending document by Id","text":"

Using the getById method to get a trending document by Id.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst trendingDoc = await graph.me.insights.trending.getById('Id')()\n\nconst trendingDoc = await graph.users.getById(\"userId\").insights.trending.getById('Id')()\n
"},{"location":"graph/insights/#get-the-resource-from-trending-document","title":"Get the resource from Trending document","text":"

Using the resources method to get the resource from a trending document.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst resource = await graph.me.insights.trending.getById('Id').resource()\n\nconst resource = await graph.users.getById(\"userId\").insights.trending.getById('Id').resource()\n
"},{"location":"graph/insights/#get-all-used-documents","title":"Get all Used documents","text":"

Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst used = await graph.me.insights.used()\n\nconst used = await graph.users.getById(\"userId\").insights.used()\n
"},{"location":"graph/insights/#get-a-used-document-by-id","title":"Get a Used document by Id","text":"

Using the getById method to get a used document by Id.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst usedDoc = await graph.me.insights.used.getById('Id')()\n\nconst usedDoc = await graph.users.getById(\"userId\").insights.used.getById('Id')()\n
"},{"location":"graph/insights/#get-the-resource-from-used-document","title":"Get the resource from Used document","text":"

Using the resources method to get the resource from a used document.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst resource = await graph.me.insights.used.getById('Id').resource()\n\nconst resource = await graph.users.getById(\"userId\").insights.used.getById('Id').resource()\n
"},{"location":"graph/insights/#get-all-shared-documents","title":"Get all Shared documents","text":"

Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst shared = await graph.me.insights.shared()\n\nconst shared = await graph.users.getById(\"userId\").insights.shared()\n
"},{"location":"graph/insights/#get-a-shared-document-by-id","title":"Get a Shared document by Id","text":"

Using the getById method to get a shared document by Id.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst sharedDoc = await graph.me.insights.shared.getById('Id')()\n\nconst sharedDoc = await graph.users.getById(\"userId\").insights.shared.getById('Id')()\n
"},{"location":"graph/insights/#get-the-resource-from-a-shared-document","title":"Get the resource from a Shared document","text":"

Using the resources method to get the resource from a shared document.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/insights\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst resource = await graph.me.insights.shared.getById('Id').resource()\n\nconst resource = await graph.users.getById(\"userId\").insights.shared.getById('Id').resource()\n
"},{"location":"graph/invitations/","title":"@pnp/graph/invitations","text":"

The ability invite an external user via the invitation manager

"},{"location":"graph/invitations/#iinvitations","title":"IInvitations","text":""},{"location":"graph/invitations/#create-invitation","title":"Create Invitation","text":"

Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/invitations\";\n\nconst graph = graphfi(...);\n\nconst invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');\n\n
"},{"location":"graph/items/","title":"@pnp/graph/items","text":"

Currently, there is no module in graph to access all items directly. Please, instead, default to search by path using the following methods.

"},{"location":"graph/items/#get-list-items","title":"Get list items","text":"
import { Site } from \"@pnp/graph/sites\";\n\nconst sites = graph.sites.getById(\"{site id}\");\n\nconst items = await Site(sites, \"lists/{listid}/items\")();\n
"},{"location":"graph/items/#get-fileitem-version-information","title":"Get File/Item version information","text":"
import { Site } from \"@pnp/graph/sites\";\n\nconst sites = graph.sites.getById(\"{site id}\");\n\nconst users = await Site(sites, \"lists/{listid}/items/{item id}/versions\")();\n
"},{"location":"graph/items/#get-list-items-with-fields-included","title":"Get list items with fields included","text":"
import { Site } from \"@pnp/graph/sites\";\nimport \"@pnp/graph/lists\";\n\nconst sites = graph.sites.getById(\"{site id}\");\n\nconst listItems : IList[] = await Site(sites, \"lists/{site id}/items?$expand=fields\")();\n
"},{"location":"graph/items/#hint-note-that-you-can-just-use-normal-graph-queries-in-this-search","title":"Hint: Note that you can just use normal graph queries in this search.","text":""},{"location":"graph/lists/","title":"@pnp/graph/lists","text":"

More information can be found in the official Graph documentation:

  • List Resource Type

"},{"location":"graph/lists/#get-lists","title":"Get Lists","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst siteLists = await graph.site.getById(\"{site identifier}\").lists();\n
"},{"location":"graph/lists/#get-list-by-id","title":"Get List by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst listInfo = await graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\")();\n
"},{"location":"graph/lists/#add-a-list","title":"Add a List","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst sampleList: List = {\n    displayName: \"PnPGraphTestList\",\n    list: { \"template\": \"genericList\" },\n};\n\nconst list = await graph.sites.getById(\"{site identifier}\").lists.add(listTemplate);\n
"},{"location":"graph/lists/#update-a-list","title":"Update a List","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst list = await graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").update({ displayName: \"MyNewListName\" });\n
"},{"location":"graph/lists/#delete-a-list","title":"Delete a List","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nawait graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").delete();\n
"},{"location":"graph/lists/#get-list-columns","title":"Get List Columns","text":"

For more information about working please see documentation on columns

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/lists\";\nimport \"@pnp/graph/columns\";\n\nconst graph = graphfi(...);\n\nawait graph.sites.getById(\"{site identifier}\").lists.getById(\"{list identifier}\").columns();\n
"},{"location":"graph/lists/#get-list-items","title":"Get List Items","text":"

Currently, recieving list items via @pnpjs/graph API is not possible.

This can currently be done with a call by path as documented under @pnpjs/graph/items

"},{"location":"graph/messages/","title":"Graph Messages (Mail)","text":"

More information can be found in the official Graph documentation:

  • Message Resource Type

"},{"location":"graph/messages/#get-users-messages","title":"Get User's Messages","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/messages\";\n\nconst graph = graphfi(...);\n\nconst currentUser = graph.me;\nconst messages = await currentUser.messages();\n
"},{"location":"graph/onedrive/","title":"@pnp/graph/onedrive","text":"

The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive.

"},{"location":"graph/onedrive/#iinvitations","title":"IInvitations","text":""},{"location":"graph/onedrive/#get-the-default-drive","title":"Get the default drive","text":"

Using the drive you can get the users default drive from Onedrive, or the groups or sites default document library.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst otherUserDrive = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive();\n\nconst currentUserDrive = await graph.me.drive();\n\nconst groupDrive = await graph.groups.getById(\"{group identifier}\").drive();\n\nconst siteDrive = await graph.sites.getById(\"{site identifier}\").drive();\n
"},{"location":"graph/onedrive/#get-all-of-the-drives","title":"Get all of the drives","text":"

Using the drives() you can get the users available drives from Onedrive

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst otherUserDrive = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives();\n\nconst currentUserDrive = await graph.me.drives();\n\nconst groupDrives = await graph.groups.getById(\"{group identifier}\").drives();\n\nconst siteDrives = await graph.sites.getById(\"{site identifier}\").drives();\n\n
"},{"location":"graph/onedrive/#get-drive-by-id","title":"Get drive by Id","text":"

Using the drives.getById() you can get one of the available drives in Outlook

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst drive = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\")();\n\nconst drive = await graph.me.drives.getById(\"{drive id}\")();\n\nconst drive = await graph.drives.getById(\"{drive id}\")();\n\n
"},{"location":"graph/onedrive/#get-the-associated-list-of-a-drive","title":"Get the associated list of a drive","text":"

Using the list() you get the associated list information

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst list = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").list();\n\nconst list = await graph.me.drives.getById(\"{drive id}\").list();\n\n

Using the getList(), from the lists implementation, you get the associated IList object. Form more infomration about acting on the IList object see @pnpjs/graph/lists

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/lists\";\n\nconst graph = graphfi(...);\n\nconst listObject: IList = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").getList();\n\nconst listOBject: IList = await graph.me.drives.getById(\"{drive id}\").getList();\n\nconst list = await listObject();\n
"},{"location":"graph/onedrive/#get-the-recent-files","title":"Get the recent files","text":"

Using the recent() you get the recent files

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst files = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").recent();\n\nconst files = await graph.me.drives.getById(\"{drive id}\").recent();\n\n
"},{"location":"graph/onedrive/#get-the-files-shared-with-me","title":"Get the files shared with me","text":"

Using the sharedWithMe() you get the files shared with the user

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst shared = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").sharedWithMe();\n\nconst shared = await graph.me.drives.getById(\"{drive id}\").sharedWithMe();\n\n// By default, sharedWithMe return items shared within your own tenant. To include items shared from external tenants include the options object.\n\nconst options: ISharingWithMeOptions = {allowExternal: true};\nconst shared = await graph.me.drives.getById(\"{drive id}\").sharedWithMe(options);\n\n
"},{"location":"graph/onedrive/#get-the-following-files","title":"Get the following files","text":"

List the items that have been followed by the signed in user.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst files = await graph.me.drives.getById(\"{drive id}\").following();\n\n
"},{"location":"graph/onedrive/#get-the-root-folder","title":"Get the Root folder","text":"

Using the root() you get the root folder

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/sites\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst root = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root();\nconst root = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive.root();\n\nconst root = await graph.me.drives.getById(\"{drive id}\").root();\nconst root = await graph.me.drive.root();\n\nconst root = await graph.sites.getById(\"{site id}\").drives.getById(\"{drive id}\").root();\nconst root = await graph.sites.getById(\"{site id}\").drive.root();\n\nconst root = await graph.groups.getById(\"{site id}\").drives.getById(\"{drive id}\").root();\nconst root = await graph.groups.getById(\"{site id}\").drive.root();\n\n
"},{"location":"graph/onedrive/#get-the-children","title":"Get the Children","text":"

Using the children() you get the children

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst rootChildren = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.children();\n\nconst rootChildren = await graph.me.drives.getById(\"{drive id}\").root.children();\n\nconst itemChildren = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").children();\n\nconst itemChildren = await graph.me.drives.getById(\"{drive id}\").root.items.getById(\"{item id}\").children();\n\n
"},{"location":"graph/onedrive/#get-the-children-by-path","title":"Get the children by path","text":"

Using the drive.getItemsByPath() you can get the contents of a particular folder path

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst item = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getItemsByPath(\"MyFolder/MySubFolder\")();\n\nconst item = await graph.me.drives.getItemsByPath(\"MyFolder/MySubFolder\")();\n\n
"},{"location":"graph/onedrive/#add-item","title":"Add Item","text":"

Using the add you can add an item, for more options please user the upload method instead.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/users\";\nimport {IDriveItemAddResult} from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst add1: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.children.add(\"test.txt\", \"My File Content String\");\nconst add2: IDriveItemAddResult = await graph.me.drives.getById(\"{drive id}\").root.children.add(\"filename.txt\", \"My File Content String\");\n
"},{"location":"graph/onedrive/#uploadreplace-drive-item-content","title":"Upload/Replace Drive Item Content","text":"

Using the .upload method you can add or update the content of an item.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/users\";\nimport {IFileOptions, IDriveItemAddResult} from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// file path is only file name\nconst fileOptions: IFileOptions = {\n    content: \"This is some test content\",\n    filePathName: \"pnpTest.txt\",\n    contentType: \"text/plain;charset=utf-8\"\n}\n\nconst uDriveRoot: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive.root.upload(fileOptions);\n\nconst uFolder: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drive.getItemById(\"{folder id}\").upload(fileOptions);\n\nconst uDriveIdRoot: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.upload(fileOptions);\n\n// file path includes folders\nconst fileOptions2: IFileOptions = {\n    content: \"This is some test content\",\n    filePathName: \"folderA/pnpTest.txt\",\n    contentType: \"text/plain;charset=utf-8\"\n}\n\nconst uFileOptions: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.upload(fileOptions2);\n
"},{"location":"graph/onedrive/#add-folder","title":"Add folder","text":"

Using addFolder you can add a folder

import { graph } from \"@pnp/graph\";\nimport \"@pnp/graph/onedrive\";\nimport \"@pnp/graph/users\"\nimport {IDriveItemAddResult} from \"@pnp/graph/ondrive\";\n\nconst graph = graphfi(...);\n\nconst addFolder1: IDriveItemAddResult = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.children.addFolder('New Folder');\nconst addFolder2: IDriveItemAddResult = await graph.me.drives.getById(\"{drive id}\").root.children.addFolder('New Folder');\n\n
"},{"location":"graph/onedrive/#search-items","title":"Search items","text":"

Using the search() you can search for items, and optionally select properties

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Where searchTerm is the query text used to search for items.\n// Values may be matched across several fields including filename, metadata, and file content.\n\nconst search = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.search(searchTerm)();\n\nconst search = await graph.me.drives.getById(\"{drive id}\").root.search(searchTerm)();\n\n
"},{"location":"graph/onedrive/#get-specific-item-in-drive","title":"Get specific item in drive","text":"

Using the items.getById() you can get a specific item from the current drive

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst item = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\")();\n\nconst item = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\")();\n\n
"},{"location":"graph/onedrive/#get-specific-item-in-drive-by-path","title":"Get specific item in drive by path","text":"

Using the drive.getItemByPath() you can get a specific item from the current drive

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst item = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getItemByPath(\"MyFolder/MySubFolder/myFile.docx\")();\n\nconst item = await graph.me.drives.getItemByPath(\"MyFolder/MySubFolder/myFile.docx\")();\n\n
"},{"location":"graph/onedrive/#get-drive-item-contents","title":"Get drive item contents","text":"

Using the item.getContent() you can get the content of a file.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nprivate _readFileAsync(file: Blob): Promise<ArrayBuffer> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      resolve(reader.result as ArrayBuffer);\n    };\n    reader.onerror = reject;\n    reader.readAsArrayBuffer(file);\n  });\n}\n\n// Where itemId is the id of the item\nconst fileContents: Blob = await graph.me.drive.getItemById(itemId).getContent();\nconst content: ArrayBuffer = await this._readFileAsync(fileContents);\n\n// This is an example of decoding plain text from the ArrayBuffer\nconst decoder = new TextDecoder('utf-8');\nconst decodedContent = decoder.decode(content);\n
"},{"location":"graph/onedrive/#convert-drive-item-contents","title":"Convert drive item contents","text":"

Using the item.convertContent() you can get a PDF version of the file. See official documentation for supported file types.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nprivate _readFileAsync(file: Blob): Promise<ArrayBuffer> {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      resolve(reader.result as ArrayBuffer);\n    };\n    reader.onerror = reject;\n    reader.readAsArrayBuffer(file);\n  });\n}\n\n// Where itemId is the id of the item\nconst fileContents: Blob = await graph.me.drive.getItemById(itemId).convertContent(\"pdf\");\nconst content: ArrayBuffer = await this._readFileAsync(fileContents);\n\n// Further manipulation of the array buffer will be needed based on your requriements.\n
"},{"location":"graph/onedrive/#get-thumbnails","title":"Get thumbnails","text":"

Using the thumbnails() you get the thumbnails

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst thumbs = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").thumbnails();\n\nconst thumbs = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").thumbnails();\n\n
"},{"location":"graph/onedrive/#delete-drive-item","title":"Delete drive item","text":"

Using the delete() you delete the current item

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst thumbs = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").delete();\n\nconst thumbs = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").delete();\n\n
"},{"location":"graph/onedrive/#update-drive-item-metadata","title":"Update drive item metadata","text":"

Using the update() you update the current item

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\nconst update = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").update({name: \"New Name\"});\n\nconst update = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").update({name: \"New Name\"});\n\n
"},{"location":"graph/onedrive/#move-drive-item","title":"Move drive item","text":"

Using the move() you move the current item, and optionally update it

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Requires a parentReference to the destination folder location\nconst moveOptions: IItemOptions = {\n  parentReference: {\n    id?: {parentLocationId};\n    driveId?: {parentLocationDriveId}};\n  };\n  name?: {newName};\n};\n\nconst move = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").move(moveOptions);\n\nconst move = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").move(moveOptions);\n\n
"},{"location":"graph/onedrive/#copy-drive-item","title":"Copy drive item","text":"

Using the copy() you can copy the current item to a new location, returns the path to the new location

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Requires a parentReference to the destination folder location\nconst copyOptions: IItemOptions = {\n  parentReference: {\n    id?: {parentLocationId};\n    driveId?: {parentLocationDriveId}};\n  };\n  name?: {newName};\n};\n\nconst copy = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").copy(copyOptions);\n\nconst copy = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").copy(copyOptions);\n\n
"},{"location":"graph/onedrive/#get-the-users-special-folders","title":"Get the users special folders","text":"

Using the users default drive you can get special folders, including: Documents, Photos, CameraRoll, AppRoot, Music

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { SpecialFolder, IDriveItem } from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Get the special folder (App Root)\nconst driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.AppRoot)();\n\n// Get the special folder (Documents)\nconst driveItem: IDriveItem = await graph.me.drive.special(SpecialFolder.Documents)();\n\n// ETC\n
"},{"location":"graph/onedrive/#get-drive-item-preview","title":"Get drive item preview","text":"

This action allows you to obtain a short-lived embeddable URL for an item in order to render a temporary preview.

If you want to obtain long-lived embeddable links, use the createLink API instead.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { IPreviewOptions, IDriveItemPreviewInfo } from \"@pnp/graph/onedrive\";\nimport { ItemPreviewInfo } from \"@microsoft/microsoft-graph-types\"\n\nconst graph = graphfi(...);\n\nconst preview: ItemPreviewInfo = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").preview();\n\nconst preview: ItemPreviewInfo = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").preview();\n\nconst previewOptions: IPreviewOptions = {\n    page: 1,\n    zoom: 90\n}\n\nconst preview2: ItemPreviewInfo = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").preview(previewOptions);\n\n
"},{"location":"graph/onedrive/#track-changes","title":"Track Changes","text":"

Track changes in a driveItem and its children over time.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { IDeltaItems } from \"@pnp/graph/ondrive\";\n\nconst graph = graphfi(...);\n\n// Get the changes for the drive items from inception\nconst delta: IDeltaItems = await graph.me.drive.root.delta()();\nconst delta: IDeltaItems = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").root.delta()();\n\n// Get the changes for the drive items from token\nconst delta: IDeltaItems = await graph.me.drive.root.delta(\"{token}\")();\n
"},{"location":"graph/onedrive/#get-drive-item-analytics","title":"Get Drive Item Analytics","text":"

Using the analytics() you get the ItemAnalytics for a DriveItem

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/onedrive\";\nimport { IAnalyticsOptions } from \"@pnp/graph/onedrive\";\n\nconst graph = graphfi(...);\n\n// Defaults to lastSevenDays\nconst analytics = await graph.users.getById(\"user@tenant.onmicrosoft.com\").drives.getById(\"{drive id}\").items.getById(\"{item id}\").analytics()();\n\nconst analytics = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").analytics()();\n\nconst analyticOptions: IAnalyticsOptions = {\n    timeRange: \"allTime\"\n};\n\nconst analyticsAllTime = await graph.me.drives.getById(\"{drive id}\").items.getById(\"{item id}\").analytics(analyticOptions)();\n
"},{"location":"graph/outlook/","title":"@pnp/graph/outlook","text":"

Represents the Outlook services available to a user. Currently, only interacting with categories is supported.

You can learn more by reading the Official Microsoft Graph Documentation.

"},{"location":"graph/outlook/#iusers-iuser-ipeople","title":"IUsers, IUser, IPeople","text":""},{"location":"graph/outlook/#get-all-categories-user","title":"Get All Categories User","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\n\nconst graph = graphfi(...);\n\n// Delegated permissions\nconst categories = await graph.me.outlook.masterCategories();\n// Application permissions\nconst categories = await graph.users.getById('{user id}').outlook.masterCategories();\n
"},{"location":"graph/outlook/#add-category-user","title":"Add Category User","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\n\nconst graph = graphfi(...);\n\n// Delegated permissions\nawait graph.me.outlook.masterCategories.add({\n  displayName: 'Newsletters', \n  color: 'preset2'\n});\n// Application permissions\nawait graph.users.getById('{user id}').outlook.masterCategories.add({\n  displayName: 'Newsletters', \n  color: 'preset2'\n});\n
"},{"location":"graph/outlook/#update-category","title":"Update Category","text":"

Testing has shown that displayName cannot be updated.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\nimport { OutlookCategory } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst categoryUpdate: OutlookCategory = {\n    color: \"preset5\"\n}\n\n// Delegated permissions\nconst categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate);\n// Application permissions\nconst categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);\n
"},{"location":"graph/outlook/#delete-category","title":"Delete Category","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/outlook\";\n\nconst graph = graphfi(...);\n\n// Delegated permissions\nconst categories = await graph.me.outlook.masterCategories.getById('{category id}').delete();\n// Application permissions\nconst categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();\n
"},{"location":"graph/photos/","title":"@pnp/graph/photos","text":"

A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64.

You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.

"},{"location":"graph/photos/#iphoto","title":"IPhoto","text":""},{"location":"graph/photos/#current-user-photo","title":"Current User Photo","text":"

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.me.photo.getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-user-photo-by-size","title":"Current User Photo by Size","text":"

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.me.photos.getBySize(\"48x48\").getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-group-photo","title":"Current Group Photo","text":"

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-group-photo-by-size","title":"Current Group Photo by Size","text":"

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photos.getBySize(\"120x120\").getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#current-team-photo","title":"Current Team Photo","text":"

This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst photoValue = await graph.teams.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob();\nconst url = window.URL || window.webkitURL;\nconst blobUrl = url.createObjectURL(photoValue);\ndocument.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);\n
"},{"location":"graph/photos/#set-user-photo","title":"Set User Photo","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst input = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst file = input.files[0];\nawait graph.me.photo.setContent(file);\n
"},{"location":"graph/photos/#set-group-photo","title":"Set Group Photo","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst input = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst file = input.files[0];\nawait graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.setContent(file);\n
"},{"location":"graph/photos/#set-team-photo","title":"Set Team Photo","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst input = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst file = input.files[0];\nawait graph.teams.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.setContent(file);\n
"},{"location":"graph/planner/","title":"@pnp/graph/planner","text":"

The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner.

"},{"location":"graph/planner/#iinvitations","title":"IInvitations","text":""},{"location":"graph/planner/#get-plans-by-id","title":"Get Plans by Id","text":"

Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst plan = await graph.planner.plans.getById('planId')();\n\n
"},{"location":"graph/planner/#add-new-plan","title":"Add new Plan","text":"

Using the planner.plans.add() you can create a new Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst newPlan = await graph.planner.plans.add('groupObjectId', 'title');\n\n
"},{"location":"graph/planner/#get-tasks-in-plan","title":"Get Tasks in Plan","text":"

Using the tasks() you can get the Tasks in a Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planTasks = await graph.planner.plans.getById('planId').tasks();\n\n
"},{"location":"graph/planner/#get-buckets-in-plan","title":"Get Buckets in Plan","text":"

Using the buckets() you can get the Buckets in a Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planBuckets = await graph.planner.plans.getById('planId').buckets();\n\n
"},{"location":"graph/planner/#get-details-in-plan","title":"Get Details in Plan","text":"

Using the details() you can get the details in a Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planDetails = await graph.planner.plans.getById('planId').details();\n\n
"},{"location":"graph/planner/#delete-plan","title":"Delete Plan","text":"

Using the delete() you can get delete a Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst delPlan = await graph.planner.plans.getById('planId').delete('planEtag');\n\n
"},{"location":"graph/planner/#update-plan","title":"Update Plan","text":"

Using the update() you can get update a Plan.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});\n\n
"},{"location":"graph/planner/#get-all-my-tasks-from-all-plans","title":"Get All My Tasks from all plans","text":"

Using the tasks() you can get the Tasks across all plans

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst planTasks = await graph.me.tasks()\n\n
"},{"location":"graph/planner/#get-task-by-id","title":"Get Task by Id","text":"

Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst task = await graph.planner.tasks.getById('taskId')();\n\n
"},{"location":"graph/planner/#add-new-task","title":"Add new Task","text":"

Using the planner.tasks.add() you can create a new Task.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst newTask = await graph.planner.tasks.add('planId', 'title');\n\n
"},{"location":"graph/planner/#get-details-in-task","title":"Get Details in Task","text":"

Using the details() you can get the details in a Task.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst taskDetails = await graph.planner.tasks.getById('taskId').details();\n\n
"},{"location":"graph/planner/#delete-task","title":"Delete Task","text":"

Using the delete() you can get delete a Task.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');\n\n
"},{"location":"graph/planner/#update-task","title":"Update Task","text":"

Using the update() you can get update a Task.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});\n\n
"},{"location":"graph/planner/#get-buckets-by-id","title":"Get Buckets by Id","text":"

Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst bucket = await graph.planner.buckets.getById('bucketId')();\n\n
"},{"location":"graph/planner/#add-new-bucket","title":"Add new Bucket","text":"

Using the planner.buckets.add() you can create a new Bucket.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst newBucket = await graph.planner.buckets.add('name', 'planId');\n\n
"},{"location":"graph/planner/#update-bucket","title":"Update Bucket","text":"

Using the update() you can get update a Bucket.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst updBucket = await graph.planner.buckets.getById('bucketId').update({name: \"Name\", eTag:'bucketEtag'});\n\n
"},{"location":"graph/planner/#delete-bucket","title":"Delete Bucket","text":"

Using the delete() you can get delete a Bucket.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');\n\n
"},{"location":"graph/planner/#get-bucket-tasks","title":"Get Bucket Tasks","text":"

Using the tasks() you can get Tasks in a Bucket.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();\n\n
"},{"location":"graph/planner/#get-plans-for-a-group","title":"Get Plans for a group","text":"

Gets all the plans for a group

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/groups\";\nimport \"@pnp/graph/planner\";\n\nconst graph = graphfi(...);\n\nconst plans = await graph.groups.getById(\"b179a282-9f94-4bb5-a395-2a80de5a5a78\").plans();\n\n
"},{"location":"graph/search/","title":"@pnp/graph/search","text":"

The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below.

"},{"location":"graph/search/#call-graphquery","title":"Call graph.query","text":"

This example shows calling the search API via the query method of the root graph object.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/search\";\n\nconst graph = graphfi(...);\n\nconst results = await graph.query({\n    entityTypes: [\"site\"],\n    query: {\n        queryString: \"test\"\n    },\n});\n

Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.

"},{"location":"graph/shares/","title":"@pnp/graph/shares","text":"

The shares module allows you to access shared files, or any file in the tenant using encoded file urls.

"},{"location":"graph/shares/#access-a-share-by-id","title":"Access a Share by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/shares\";\n\nconst graph = graphfi(...);\n\nconst shareInfo = await graph.shares.getById(\"{shareId}\")();\n
"},{"location":"graph/shares/#encode-a-sharing-link","title":"Encode a Sharing Link","text":"

If you don't have a share id but have the absolute path to a file you can encode it into a sharing link, allowing you to access it directly using the /shares endpoint.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/shares\";\n\nconst graph = graphfi(...);\n\nconst shareLink: string = graph.shares.encodeSharingLink(\"https://{tenant}.sharepoint.com/sites/dev/Shared%20Documents/new.pptx\");\n\nconst shareInfo = await graph.shares.getById(shareLink)();\n
"},{"location":"graph/shares/#access-a-shares-driveitem-resource","title":"Access a Share's driveItem resource","text":"

You can also access the full functionality of the driveItem via a share. Find more details on the capabilities of driveItem here.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/shares\";\n\nconst graph = graphfi(...);\n\nconst driveItemInfo = await graph.shares.getById(\"{shareId}\").driveItem();\n
"},{"location":"graph/sites/","title":"@pnp/graph/sites","text":"

The search module allows you to access the Microsoft Graph Sites API.

"},{"location":"graph/sites/#call-graphsites","title":"Call graph.sites","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\n\nconst graph = graphfi(...);\n\nconst sitesInfo = await graph.sites();\n
"},{"location":"graph/sites/#call-graphsitesgetbyid","title":"Call graph.sites.getById","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\n\nconst graph = graphfi(...);\n\nconst siteInfo = await graph.sites.getById(\"{site identifier}\")();\n
"},{"location":"graph/sites/#call-graphsitesgetbyurl","title":"Call graph.sites.getByUrl","text":"

Using the sites.getByUrl() you can get a site using url instead of identifier

If you get a site with this method, the graph does not support chaining a request further than .drive. We will review and try and create a work around for this issue.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/sites\";\n\nconst graph = graphfi(...);\nconst sharepointHostName = \"contoso.sharepoint.com\";\nconst serverRelativeUrl = \"/sites/teamsite1\";\nconst siteInfo = await graph.sites.getByUrl(sharepointHostName, serverRelativeUrl)();\n
"},{"location":"graph/sites/#make-additional-calls-or-recieve-items-from-lists","title":"Make additional calls or recieve items from lists","text":"

We don't currently implement all of the available options in graph for sites, rather focusing on the sp library. While we do accept PRs to add functionality, you can also make calls by path.

"},{"location":"graph/subscriptions/","title":"@pnp/graph/subscriptions","text":"

The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft graph. Currently, subscriptions are enabled for the following resources:

  • Mail, events, and contacts from Outlook.
  • Conversations from Office Groups.
  • Drive root items from OneDrive.
  • Users and Groups from Azure Active Directory.
  • Alerts from the Microsoft Graph Security API.
"},{"location":"graph/subscriptions/#get-all-of-the-subscriptions","title":"Get all of the Subscriptions","text":"

Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst subscriptions = await graph.subscriptions();\n\n
"},{"location":"graph/subscriptions/#create-a-new-subscription","title":"Create a new Subscription","text":"

Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst addedSubscription = await graph.subscriptions.add(\"created,updated\", \"https://webhook.azurewebsites.net/api/send/myNotifyClient\", \"me/mailFolders('Inbox')/messages\", \"2019-11-20T18:23:45.9356913Z\");\n\n
"},{"location":"graph/subscriptions/#get-subscription-by-id","title":"Get Subscription by Id","text":"

Using the subscriptions.getById() you can get one of the subscriptions

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst subscription = await graph.subscriptions.getById('subscriptionId')();\n\n
"},{"location":"graph/subscriptions/#delete-a-subscription","title":"Delete a Subscription","text":"

Using the subscriptions.getById().delete() you can remove one of the Subscriptions

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst delSubscription = await graph.subscriptions.getById('subscriptionId').delete();\n\n
"},{"location":"graph/subscriptions/#update-a-subscription","title":"Update a Subscription","text":"

Using the subscriptions.getById().update() you can update one of the Subscriptions

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/subscriptions\";\n\nconst graph = graphfi(...);\n\nconst updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: \"created,updated,deleted\" });\n\n
"},{"location":"graph/teams/","title":"@pnp/graph/teams","text":"

The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams.

"},{"location":"graph/teams/#teams-the-user-is-a-member-of","title":"Teams the user is a member of","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams();\n\nconst myJoinedTeams = await graph.me.joinedTeams();\n\n
"},{"location":"graph/teams/#get-teams-by-id","title":"Get Teams by Id","text":"

Using the teams.getById() you can get a specific Team.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();\n
"},{"location":"graph/teams/#create-new-teamgroup-method-1","title":"Create new Team/Group - Method #1","text":"

The first way to create a new Team and corresponding Group is to first create the group and then create the team. Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.

"},{"location":"graph/teams/#create-a-team-via-a-specific-group","title":"Create a Team via a specific group","text":"

Here we get the group via id and use createTeam

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport \"@pnp/graph/groups\";\n\nconst graph = graphfi(...);\n\nconst createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({\n\"memberSettings\": {\n    \"allowCreateUpdateChannels\": true\n},\n\"messagingSettings\": {\n        \"allowUserEditMessages\": true,\n\"allowUserDeleteMessages\": true\n},\n\"funSettings\": {\n    \"allowGiphy\": true,\n    \"giphyContentRating\": \"strict\"\n}});\n
"},{"location":"graph/teams/#create-new-teamgroup-method-2","title":"Create new Team/Group - Method #2","text":"

The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst team = {\n        \"template@odata.bind\": \"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\",\n        \"displayName\": \"PnPJS Test Team\",\n        \"description\": \"PnPJS Test Team\u2019s Description\",\n        \"members\": [\n            {\n                \"@odata.type\": \"#microsoft.graph.aadUserConversationMember\",\n                \"roles\": [\"owner\"],\n                \"user@odata.bind\": \"https://graph.microsoft.com/v1.0/users('{owners user id}')\",\n            },\n        ],\n    };\n\nconst createdTeam: ITeamCreateResultAsync = await graph.teams.create(team);\n//To check the status of the team creation, call getOperationById for the newly created team.\nconst createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);\n
"},{"location":"graph/teams/#clone-a-team","title":"Clone a Team","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(\n'Cloned','description','apps,tabs,settings,channels,members','public');\n\n
"},{"location":"graph/teams/#get-teams-async-operation","title":"Get Teams Async Operation","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(\n'Cloned','description','apps,tabs,settings,channels,members','public');\nconst clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);\n
"},{"location":"graph/teams/#archive-a-team","title":"Archive a Team","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();\n
"},{"location":"graph/teams/#unarchive-a-team","title":"Unarchive a Team","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();\n
"},{"location":"graph/teams/#get-all-channels-of-a-team","title":"Get all channels of a Team","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();\n
"},{"location":"graph/teams/#get-primary-channel","title":"Get primary channel","text":"

Using the teams.getById() you can get a specific Team.

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\nconst channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').primaryChannel();\n
"},{"location":"graph/teams/#get-channel-by-id","title":"Get channel by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();\n\n
"},{"location":"graph/teams/#create-a-new-channel","title":"Create a new Channel","text":"
import { graphfi } from \"@pnp/graph\";\n\nconst graph = graphfi(...);\n\nconst newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');\n\n
"},{"location":"graph/teams/#list-messages","title":"List Messages","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst chatMessages = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages();\n
"},{"location":"graph/teams/#add-chat-message-to-channel","title":"Add chat message to Channel","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\nimport { ChatMessage } from \"@microsoft/microsoft-graph-types\";\n\nconst graph = graphfi(...);\n\nconst message = {\n      \"body\": {\n        \"content\": \"Hello World\"\n      }\n    }\nconst chatMessage: ChatMessage = await graph.teams.getById('3531fzfb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384xa89c81115c281428a3@thread.skype').messages.add(message);\n
"},{"location":"graph/teams/#get-installed-apps","title":"Get installed Apps","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();\n\n
"},{"location":"graph/teams/#add-an-app","title":"Add an App","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');\n\n
"},{"location":"graph/teams/#remove-an-app","title":"Remove an App","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.delete();\n\n
"},{"location":"graph/teams/#get-tabs-from-a-channel","title":"Get Tabs from a Channel","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();\n\n
"},{"location":"graph/teams/#get-tab-by-id","title":"Get Tab by Id","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();\n\n
"},{"location":"graph/teams/#add-a-new-tab-to-channel","title":"Add a new Tab to Channel","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{});\n\n
"},{"location":"graph/teams/#update-a-tab","title":"Update a Tab","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').update({\n    displayName: \"New tab name\"\n});\n\n
"},{"location":"graph/teams/#remove-a-tab-from-channel","title":"Remove a Tab from channel","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/teams\";\n\nconst graph = graphfi(...);\n\nconst tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').\nchannels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id').delete();\n\n
"},{"location":"graph/teams/#team-membership","title":"Team Membership","text":"

Get the members and/or owners of a group.

See Groups

"},{"location":"graph/users/","title":"@pnp/graph/users","text":"

Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services.

You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.

"},{"location":"graph/users/#iusers-iuser-ipeople","title":"IUsers, IUser, IPeople","text":""},{"location":"graph/users/#current-user","title":"Current User","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst currentUser = await graph.me();\n
"},{"location":"graph/users/#get-users-in-the-organization","title":"Get Users in the Organization","text":"

If you want to get all users you will need to use paging

import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst allUsers = await graph.users();\n
"},{"location":"graph/users/#get-a-user-by-email-address-or-user-id","title":"Get a User by email address (or user id)","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst matchingUser = await graph.users.getById('jane@contoso.com')();\n
"},{"location":"graph/users/#user-properties","title":"User Properties","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nawait graph.me.memberOf();\nawait graph.me.transitiveMemberOf();\n
"},{"location":"graph/users/#update-current-user","title":"Update Current User","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nawait graph.me.update({\n    displayName: 'John Doe'\n});\n
"},{"location":"graph/users/#people","title":"People","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst people = await graph.me.people();\n\n// get the top 3 people\nconst people = await graph.me.people.top(3)();\n
"},{"location":"graph/users/#manager","title":"Manager","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst manager = await graph.me.manager();\n
"},{"location":"graph/users/#direct-reports","title":"Direct Reports","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\n\nconst graph = graphfi(...);\n\nconst reports = await graph.me.directReports();\n
"},{"location":"graph/users/#photo","title":"Photo","text":"
import { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users\";\nimport \"@pnp/graph/photos\";\n\nconst graph = graphfi(...);\n\nconst currentUser = await graph.me.photo();\nconst specificUser = await graph.users.getById('jane@contoso.com').photo();\n
"},{"location":"graph/users/#user-photo-operations","title":"User Photo Operations","text":"

See Photos

"},{"location":"graph/users/#user-presence-operation","title":"User Presence Operation","text":"

See Cloud Communications

"},{"location":"graph/users/#user-messages-mail","title":"User Messages (Mail)","text":"

See Messages

"},{"location":"graph/users/#user-onedrive","title":"User OneDrive","text":"

See OneDrive

"},{"location":"logging/","title":"@pnp/logging","text":"

The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.

"},{"location":"logging/#getting-started","title":"Getting Started","text":"

Install the logging module, it has no other dependencies

npm install @pnp/logging --save

"},{"location":"logging/#understanding-the-logging-framework","title":"Understanding the Logging Framework","text":"

The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter.

/**\n * Interface that defines a log listener\n *\n */\nexport interface ILogListener {\n    /**\n     * Any associated data that a given logging listener may choose to log or ignore\n     *\n     * @param entry The information to be logged\n     */\n    log(entry: ILogEntry): void;\n}\n\n/**\n * Interface that defines a log entry\n *\n */\nexport interface ILogEntry {\n    /**\n     * The main message to be logged\n     */\n    message: string;\n    /**\n     * The level of information this message represents\n     */\n    level: LogLevel;\n    /**\n     * Any associated data that a given logging listener may choose to log or ignore\n     */\n    data?: any;\n}\n
"},{"location":"logging/#log-levels","title":"Log Levels","text":"
export const enum LogLevel {\n    Verbose = 0,\n    Info = 1,\n    Warning = 2,\n    Error = 3,\n    Off = 99,\n}\n
"},{"location":"logging/#writing-to-the-logger","title":"Writing to the Logger","text":"

To write information to a logger you can use either write, writeJSON, or log.

import {\n    Logger,\n    LogLevel\n} from \"@pnp/logging\";\n\n// write logs a simple string as the message value of the LogEntry\nLogger.write(\"This is logging a simple string\");\n\n// optionally passing a level, default level is Verbose\nLogger.write(\"This is logging a simple string\", LogLevel.Error);\n\n// this will convert the object to a string using JSON.stringify and set the message with the result\nLogger.writeJSON({ name: \"value\", name2: \"value2\"});\n\n// optionally passing a level, default level is Verbose\nLogger.writeJSON({ name: \"value\", name2: \"value2\"}, LogLevel.Warning);\n\n// specify the entire LogEntry interface using log\nLogger.log({\n    data: { name: \"value\", name2: \"value2\"},\n    level: LogLevel.Warning,\n    message: \"This is my message\"\n});\n
"},{"location":"logging/#log-an-error","title":"Log an error","text":"

There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance passed in, the level will be 'Error', and the message will be the Error instance's message property.

const e = Error(\"An Error\");\n\nLogger.error(e);\n
"},{"location":"logging/#subscribing-a-listener","title":"Subscribing a Listener","text":"

By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.

import {\n    Logger,\n    ConsoleListener,\n    LogLevel\n} from \"@pnp/logging\";\n\n// subscribe a listener\nLogger.subscribe(ConsoleListener());\n\n// set the active log level\nLogger.activeLogLevel = LogLevel.Info;\n
"},{"location":"logging/#available-listeners","title":"Available Listeners","text":"

There are two listeners included in the library, ConsoleListener and FunctionListener.

"},{"location":"logging/#consolelistener","title":"ConsoleListener","text":"

This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.

"},{"location":"logging/#configuration-options","title":"Configuration Options","text":"

Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).

"},{"location":"logging/#using-a-prefix","title":"Using a Prefix","text":"

To add a prefix to all output, supply a string in the constructor:

import {\n    Logger,\n    ConsoleListener,\n    LogLevel\n} from \"@pnp/logging\";\n\nconst LOG_SOURCE: string = 'MyAwesomeWebPart';\nLogger.subscribe(ConsoleListener(LOG_SOURCE));\nLogger.activeLogLevel = LogLevel.Info;\n

With the above configuration, Logger.write(\"My special message\"); will be output to the console as:

MyAwesomeWebPart - My special message\n
"},{"location":"logging/#customizing-text-color","title":"Customizing Text Color","text":"

You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color).

Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.):

import {\n    Logger,\n    ConsoleListener,\n    LogLevel\n} from \"@pnp/logging\";\n\nconst LOG_SOURCE: string = 'MyAwesomeWebPart';\nLogger.subscribe(ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'}));\nLogger.activeLogLevel = LogLevel.Info;\n

With the above configuration:

Logger.write(\"My special message\");\nLogger.write(\"A warning!\", LogLevel.Warning);\n

Will result in messages that look like this:

Color options:

  • color: Default text color for all logging levels unless they're specified
  • verboseColor: Text color to use for messages with LogLevel.Verbose
  • infoColor: Text color to use for messages with LogLevel.Info
  • warningColor: Text color to use for messages with LogLevel.Warning
  • errorColor: Text color to use for messages with LogLevel.Error

To set colors without a prefix, specify either undefined or an empty string for the first parameter:

Logger.subscribe(ConsoleListener(undefined, {color:'purple'}));\n
"},{"location":"logging/#functionlistener","title":"FunctionListener","text":"

The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.

import {\n    Logger,\n    FunctionListener,\n    ILogEntry\n} from \"@pnp/logging\";\n\nlet listener = new FunctionListener((entry: ILogEntry) => {\n\n    // pass all logging data to an existing framework\n    MyExistingCompanyLoggingFramework.log(entry.message);\n});\n\nLogger.subscribe(listener);\n
"},{"location":"logging/#create-a-custom-listener","title":"Create a Custom Listener","text":"

If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface.

import {\n    Logger,\n    ILogListener,\n    ILogEntry\n} from \"@pnp/logging\";\n\nclass MyListener implements ILogListener {\n\n    log(entry: ILogEntry): void {\n        // here you would do something with the entry\n    }\n}\n\nLogger.subscribe(new MyListener());\n
"},{"location":"logging/#logging-behavior","title":"Logging Behavior","text":"

To allow seamless logging with v3 we have introduced the PnPLogging behavior. It takes a single augument representing the log level of that behavior, allowing you to be very selective in what logging you want to get. As well the log level applied here ignores any global level set with activeLogLevel on Logger.

import { LogLevel, PnPLogging, Logger, ConsoleListener } from \"@pnp/logging\";\nimport { spfi, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\n// subscribe a listener\nLogger.subscribe(ConsoleListener());\n\n// at the root we only want to log errors, which will be sent to all subscribed loggers on Logger\nconst sp = spfi().using(SPFx(this.context), PnPLogging(LogLevel.Error));\n\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n// use verbose logging with this particular list because you are trying to debug something\nlist.using(PnPLogging(LogLevel.Verbose));\n\nconst listData = await list();\n
"},{"location":"msaljsclient/","title":"@pnp/msaljsclient","text":"

This library provides a thin wrapper around the msal library to make it easy to integrate MSAL authentication in the browser.

You will first need to install the package:

npm install @pnp/msaljsclient --save

The configuration and authParams

import { spfi, SPBrowser } from \"@pnp/sp\";\nimport { MSAL } from \"@pnp/msaljsclient\";\nimport \"@pnp/sp/webs\";\n\nconst configuation = {\n    auth: {\n        authority: \"https://login.microsoftonline.com/common\",\n        clientId: \"{client id}\",\n    }\n};\n\nconst authParams = {\n    scopes: [\"https://{tenant}.sharepoint.com/.default\"],\n};\n\nconst sp = spfi(\"https://tenant.sharepoint.com/sites/dev\").using(SPBrowser(), MSAL(configuration, authParams));\n\nconst webData = await sp.web();\n

Please see more scenarios in the authentication article.

"},{"location":"news/2020-year-in-review/","title":"2020 Year End Report","text":"

Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.

This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules.

We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community.

Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured \"roots\" such as \"sp\" or \"graph\" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios.

Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.

"},{"location":"news/2020-year-in-review/#usage","title":"Usage","text":"

In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227.

These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November.

1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds

"},{"location":"news/2020-year-in-review/#releases","title":"Releases","text":"

We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

"},{"location":"news/2020-year-in-review/#npm-package-download-statistics-pnpsp","title":"NPM Package download statistics (@pnp/sp):","text":"Month Count * Month Count January 100,686 * July 36,805 February 34,437 * August 38,897 March 34,574 * September 45,968 April 32,436 * October 46,655 May 34,482 * November 45,511 June 34,408 * December 58,977 Grand Total 543,836

With 2020 our total all time downloads of @pnp/sp is now at: 949,638

Stats from https://npm-stat.com/

"},{"location":"news/2020-year-in-review/#future-plans","title":"Future Plans","text":"

Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date.

Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.

"},{"location":"news/2020-year-in-review/#new-lead-maintainer","title":"New Lead Maintainer","text":"

With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work.

Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean.

We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.

"},{"location":"news/2020-year-in-review/#contributors","title":"Contributors","text":"

As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.

"},{"location":"news/2020-year-in-review/#sponsors","title":"Sponsors","text":"

We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

Thank You

"},{"location":"news/2020-year-in-review/#closing","title":"Closing","text":"

In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

Wishing you the very best for 2021,

The PnPjs Team

"},{"location":"news/2021-year-in-review/","title":"2021 Year End Report","text":"

Welcome to our second year in review report for PnPjs. 2021 found us planning, building, testing, and documenting a whole new version of PnPjs. The goal is to deliver a much improved and flexible experience and none of that would have been possible without the support and participation of everyone in the PnP community - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.

Because of the huge useage we've seen with the library and issues we found implementing some of the much requested enhancements, we felt we really needed to start from the ground up and rearchitect the library completely. This new design, built on the concept of a \"Timeline\", enabled us to build a significantly lighter weight solution that is more extensible than ever. And bonus, we were able to keep the overall development experience largly unchanged, so that makes transitioning all that much easier. In addition we took extra effort to validate our development efforts by making sure all our tests passed so that we could better ensure quality of the library. Check out our Transition Guide and ChangeLog for all the details.

In other news, we fixed 47 reported bugs, answered 89 questions, and made 51 suggested enhancements to version 2 of the library - all driven by feedback from users and the community.

"},{"location":"news/2021-year-in-review/#usage","title":"Usage","text":"

In 2021 we transitioned from rapid growth to slower growth but maintaining a request/month rate over 11 billion, approaching 13 billion by the end of the year. These requests came from more than 25 thousand tenants including some of the largest M365 customers. Due to some data cleanup we don't have the full year's information, but the below graph shows the final 7 months of the year.

"},{"location":"news/2021-year-in-review/#releases","title":"Releases","text":"

We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

"},{"location":"news/2021-year-in-review/#npm-package-download-statistics-pnpsp","title":"NPM Package download statistics (@pnp/sp)","text":"Month Count * Month Count January 49,446 * July 73,491 February 56,054 * August 74,236 March 66,113 * September 69,179 April 58,526 * October 77,645 May 62,747 * November 74,966 June 69,349 * December 61,995 Grand Total 793,747

For comparison our total downloads in 2020 was 543,836.

With 2021 our total all time downloads of @pnp/sp is now at: 1,743,385

In 2020 the all time total was 949,638.

Stats from https://npm-stat.com/

"},{"location":"news/2021-year-in-review/#future-plans","title":"Future Plans","text":"

Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.

"},{"location":"news/2021-year-in-review/#contributors","title":"Contributors","text":"

As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.

"},{"location":"news/2021-year-in-review/#sponsors","title":"Sponsors","text":"

We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

Thank You

"},{"location":"news/2021-year-in-review/#closing","title":"Closing","text":"

In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

Wishing you the very best for 2022,

The PnPjs Team

"},{"location":"news/2022-year-in-review/","title":"2022 Year End Report","text":"

Wow, what a year for PnPjs! We released our latest major version 3.0 on Valentine's Day 2022 which included significant performance improvements, a completely rewritten internal architecture, and reduced the bundled library size by two-thirds. As well we continued out monthly releases bringing enhancements and bug fixes to our users on a continual basis.

But before we go any further we once again say Thank You!!! to everyone that has used, contributed to, and provided feedback on the library. This journey is not possible without you, and this last year you have driven us to be our best.

Version 3 introduces a completely new design for the internals of the library, easily allowing consumers to customize any part of the request process to their needs. Centered around an extensible Timeline and extended for http requests by Queryable this new pattern reduced code duplication, interlock, and complexity significantly. It allows everything in the request flow to be controlled through behaviors, which are plain functions acting at the various stages of the request. Using this model we reimagined batching, caching, authentication, and parsing in simpler, composable ways. If you have not yet updated to version 3, we encourage you to do so. You can review the transition guide to get started.

As one last treat, we set up nightly builds so that each day you can get a fresh version with any updates merged the previous day. This is super helpful if you're waiting for a specific fix or feature for your project. It allows for easier testing of new features through the full dev lifecycle, as well.

In other news, we fixed 54 reported bugs, answered 123 questions, and made 54 suggested enhancements to version 3 of the library - all driven by feedback from users and the community.

"},{"location":"news/2022-year-in-review/#usage","title":"Usage","text":"

In 2022 we continued to see steady usage and growth maintaining a requst/month rate over 30 billion for much of the year. These requets came from ~29K tenants a month, including some of our largest M365 customers.

"},{"location":"news/2022-year-in-review/#releases","title":"Releases","text":"

We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

"},{"location":"news/2022-year-in-review/#npm-package-download-statistics-pnpsp","title":"NPM Package download statistics (@pnp/sp)","text":"Month Count * Month Count January 70,863 * July 63,844 February 76,649 * August 75,713 March 83,902 * September 71,447 April 70,429 * October 84,744 May 72,406 * November 82,459 June 71,375 * December 65,785 Grand Total 889,616

For comparison our total downloads in 2021 was 793,747.

With 2022 our total all time downloads of @pnp/sp is now at: 2,543,639

In 2021 the all time total was 1,743,385.

Stats from https://npm-stat.com/

"},{"location":"news/2022-year-in-review/#future-plans","title":"Future Plans","text":"

Looking to the future we will continue to actively grow and improve v3 of the library, guided by feedback and reported issues. Additionally, we are looking to expand our contributions documentation to make it easier for community members to contibute their ideas and updates to the library.

"},{"location":"news/2022-year-in-review/#contributors","title":"Contributors","text":"

As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2021! If you are interested in becoming a contributor check out our guide on ways to get started.

"},{"location":"news/2022-year-in-review/#sponsors","title":"Sponsors","text":"

We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

Thank You

"},{"location":"news/2022-year-in-review/#closing","title":"Closing","text":"

In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

Wishing you the very best for 2023,

The PnPjs Team

"},{"location":"nodejs/behaviors/","title":"@pnp/nodejs : behaviors","text":"

The article describes the behaviors exported by the @pnp/nodejs library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/sp, and @pnp/graph.

"},{"location":"nodejs/behaviors/#nodefetch","title":"NodeFetch","text":"

This behavior, for use in nodejs, provides basic fetch support through the node-fetch package. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing

For fetch configuration in browsers please see @pnp/queryable behaviors.

import { NodeFetch } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetch());\n\nawait sp.webs();\n
import { NodeFetch } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetch({ replace: false }));\n\nawait sp.webs();\n
"},{"location":"nodejs/behaviors/#nodefetchwithretry","title":"NodeFetchWithRetry","text":"

This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.

import { NodeFetchWithRetry } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetchWithRetry());\n\nawait sp.webs();\n

You can also control how the behavior works through its props. The replace value works as described above for NodeFetch. interval specifies the initial dynamic back off value in milliseconds. This value is ignored if a \"Retry-After\" header exists in the response. retries indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.

import { NodeFetchWithRetry } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(NodeFetchWithRetry({\n    retries: 2,\n    interval: 400,\n    replace: true,\n}));\n\nawait sp.webs();\n
"},{"location":"nodejs/behaviors/#graphdefault","title":"GraphDefault","text":"

The GraphDefault behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse, graph's DefaultHeaders, and graph's DefaultInit. It is configured using a props argument:

interface IGraphDefaultProps {\n    baseUrl?: string;\n    msal: {\n        config: Configuration;\n        scopes?: string[];\n    };\n}\n

You can use the baseUrl property to specify either v1.0 or beta - or one of the special graph urls.

import { GraphDefault } from \"@pnp/nodejs\";\nimport { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users/index.js\";\n\nconst graph = graphfi().using(GraphDefault({\n    // use the German national graph endpoint\n    baseUrl: \"https://graph.microsoft.de/v1.0\",\n    msal: {\n        config: { /* my msal config */ },\n    }\n}));\n\nawait graph.me();\n
"},{"location":"nodejs/behaviors/#msal","title":"MSAL","text":"

This behavior provides a thin wrapper around the @azure/msal-node library. The options you provide are passed directly to msal, and all options are available.

import { MSAL } from \"@pnp/nodejs\";\nimport { graphfi } from \"@pnp/graph\";\nimport \"@pnp/graph/users/index.js\";\n\nconst graph = graphfi().using(MSAL(config: { /* my msal config */ }, scopes: [\"https://graph.microsoft.com/.default\"]);\n\nawait graph.me();\n
"},{"location":"nodejs/behaviors/#spdefault","title":"SPDefault","text":"

The SPDefault behavior is a composed behavior including MSAL, NodeFetchWithRetry, DefaultParse,sp's DefaultHeaders, and sp's DefaultInit. It is configured using a props argument:

interface ISPDefaultProps {\n    baseUrl?: string;\n    msal: {\n        config: Configuration;\n        scopes: string[];\n    };\n}\n

You can use the baseUrl property to specify the absolute site/web url to which queries should be set.

import { SPDefault } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(SPDefault({\n    msal: {\n        config: { /* my msal config */ },\n        scopes: [\"Scope.Value\", \"Scope2.Value\"],\n    }\n}));\n\nawait sp.web();\n
"},{"location":"nodejs/behaviors/#streamparse","title":"StreamParse","text":"

StreamParse is a specialized parser allowing request results to be read as a nodejs stream. The return value when using this parser will be of the shape:

{\n    body: /* The .body property of the Response object */,\n    knownLength: /* number value calculated from the Response's content-length header */\n}\n
import { StreamParse } from \"@pnp/nodejs\";\n\nimport \"@pnp/sp/webs/index.js\";\n\nconst sp = spfi().using(StreamParse());\n\nconst streamResult = await sp.someQueryThatReturnsALargeFile();\n\n// read the stream as text\nconst txt = await new Promise<string>((resolve) => {\n    let data = \"\";\n    streamResult.body.on(\"data\", (chunk) => data += chunk);\n    streamResult.body.on(\"end\", () => resolve(data));\n});\n
"},{"location":"nodejs/sp-extensions/","title":"@pnp/nodejs - sp extensions","text":"

By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api.

"},{"location":"nodejs/sp-extensions/#ifilegetstream","title":"IFile.getStream","text":"

Allows you to read a response body as a nodejs PassThrough stream.

// by importing the the library the node specific extensions are automatically applied\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(\"https://something.com\").using(SPDefault({\n    // config\n}));\n\n// get the stream\nconst streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream();\n\n// see if we have a known length\nconsole.log(streamResult.knownLength);\n\n// read the stream\n// this is a very basic example - you can do tons more with streams in node\nconst txt = await new Promise<string>((resolve) => {\n    let data = \"\";\n    stream.body.on(\"data\", (chunk) => data += chunk);\n    stream.body.on(\"end\", () => resolve(data));\n});\n
"},{"location":"nodejs/sp-extensions/#ifilesaddchunked","title":"IFiles.addChunked","text":"
import { SPDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs/index.js\";\nimport \"@pnp/sp/folders/web.js\";\nimport \"@pnp/sp/folders/list.js\";\nimport \"@pnp/sp/files/web.js\";\nimport \"@pnp/sp/files/folder.js\";\nimport * as fs from \"fs\";\n\nconst sp = spfi(\"https://something.com\").using(SPDefault({\n    // config\n}));\n\n// NOTE: you must supply the highWaterMark to determine the block size for stream uploads\nconst stream = fs.createReadStream(\"{file path}\", { highWaterMark: 10485760 });\nconst files = sp.web.defaultDocumentLibrary.rootFolder.files;\n\n// passing the chunkSize parameter has no affect when using a stream, use the highWaterMark as shown above when creating the stream\nawait files.addChunked(name, stream, null, true);\n
"},{"location":"nodejs/sp-extensions/#ifilesetstreamcontentchunked","title":"IFile.setStreamContentChunked","text":"
import { SPDefault } from \"@pnp/nodejs\";\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs/index.js\";\nimport \"@pnp/sp/folders/web.js\";\nimport \"@pnp/sp/folders/list.js\";\nimport \"@pnp/sp/files/web.js\";\nimport \"@pnp/sp/files/folder.js\";\nimport * as fs from \"fs\";\n\nconst sp = spfi(\"https://something.com\").using(SPDefault({\n    // config\n}));\n\n// NOTE: you must supply the highWaterMark to determine the block size for stream uploads\nconst stream = fs.createReadStream(\"{file path}\", { highWaterMark: 10485760 });\nconst file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName(\"file-name.txt\");\n\nawait file.setStreamContentChunked(stream);\n
"},{"location":"nodejs/sp-extensions/#explicit-import","title":"Explicit import","text":"

If you don't need to import anything from the library, but would like to include the extensions just import the library as shown.

import \"@pnp/nodejs\";\n\n// get the stream\nconst streamResult = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream();\n
"},{"location":"nodejs/sp-extensions/#accessing-sp-extension-namespace","title":"Accessing SP Extension Namespace","text":"

There are classes and interfaces included in extension modules, which you can access through a namespace, \"SPNS\".

import { SPNS } from \"@pnp/nodejs-commonjs\";\n\nconst parser = new SPNS.StreamParser();\n
"},{"location":"queryable/behaviors/","title":"@pnp/queryable : behaviors","text":"

The article describes the behaviors exported by the @pnp/queryable library. Please also see available behaviors in @pnp/core, @pnp/nodejs, @pnp/sp, and @pnp/graph.

Generally you won't need to use these behaviors individually when using the defaults supplied by the library, but when appropriate you can create your own composed behaviors using these as building blocks.

"},{"location":"queryable/behaviors/#bearer-token","title":"Bearer Token","text":"

Allows you to inject an existing bearer token into the request. This behavior will not replace any existing authentication behaviors, so you may want to ensure they are cleared if you are supplying your own tokens, regardless of their source. This behavior does no caching or performs any operation other than including your token in an authentication heading.

import { BearerToken } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BearerToken(\"HereIsMyBearerTokenStringFromSomeSource\"));\n\n// optionally clear any configured authentication as you are supplying a token so additional calls shouldn't be needed\n// but take care as other behaviors may add observers to auth\nsp.on.auth.clear();\n\n// the bearer token supplied above will be applied to all requests made from `sp`\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#browserfetch","title":"BrowserFetch","text":"

This behavior, for use in web browsers, provides basic fetch support through the browser's fetch global method. It replaces any other registered observers on the send moment by default, but this can be controlled via the props. Remember, when registering observers on the send moment only the first one will be used so not replacing

For fetch configuration in nodejs please see @pnp/nodejs behaviors.

import { BrowserFetch } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetch());\n\nconst webInfo = await sp.webs();\n
import { BrowserFetch } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetch({ replace: false }));\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#browserfetchwithretry","title":"BrowserFetchWithRetry","text":"

This behavior makes fetch requests but will attempt to retry the request on certain failures such as throttling.

import { BrowserFetchWithRetry } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetchWithRetry());\n\nconst webInfo = await sp.webs();\n

You can also control how the behavior works through its props. The replace value works as described above for BrowserFetch. interval specifies the initial dynamic back off value in milliseconds. This value is ignored if a \"Retry-After\" header exists in the response. retries indicates the number of times to retry before failing the request, the default is 3. A default of 3 will result in up to 4 total requests being the initial request and threee potential retries.

import { BrowserFetchWithRetry } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BrowserFetchWithRetry({\n    retries: 2,\n    interval: 400,\n    replace: true,\n}));\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#caching","title":"Caching","text":"

This behavior allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map. It is a good idea to include caching in your projects to improve performance. By default items in the cache will expire after 5 minutes.

import { Caching } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(Caching());\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst webInfo = await sp.webs();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst webInfo2 = await sp.webs();\n
"},{"location":"queryable/behaviors/#custom-key-function","title":"Custom Key Function","text":"

You can also supply custom functionality to control how keys are generated and calculate the expirations.

The cache key factory has the form (url: string) => string and you must ensure your keys are unique enough that you won't have collisions.

The expire date factory has the form (url: string) => Date and should return the Date when the cached data should expire. If you know that some particular data won't expire often you can set this date far in the future, or for more frequently updated information you can set it lower. If you set the expiration too short there is no reason to use caching as any stored information will likely always be expired. Additionally, you can set the storage to use local storage which will persist across sessions.

Note that for sp.search() requests if you want to specify a key you will need to use the CacheKey behavior below, the keyFactory value will be overwritten

import { getHashCode, PnPClientStorage, dateAdd, TimelinePipe } from \"@pnp/core\";\nimport { Caching } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(Caching({\n    store: \"local\",\n    // use a hascode for the key\n    keyFactory: (url) => getHashCode(url.toLowerCase()).toString(),\n    // cache for one minute\n    expireFunc: (url) => dateAdd(new Date(), \"minute\", 1),\n}));\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst webInfo = await sp.webs();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst webInfo2 = await sp.webs();\n

As with any behavior you have the option to only apply caching to certain requests:

import { getHashCode, dateAdd } from \"@pnp/core\";\nimport { Caching } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// caching will only apply to requests using `cachingList` as the base of the fluent chain\nconst cachingList = sp.web.lists.getByTitle(\"{List Title}\").using(Caching());\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst itemsInfo = await cachingList.items();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst itemsInfo2 = await cachingList.items();\n
"},{"location":"queryable/behaviors/#bindcachingcore","title":"bindCachingCore","text":"

Added in 3.10.0

The bindCachingCore method is supplied to allow all caching behaviors to share a common logic around the handling of ICachingProps. Usage of this function is not required to build your own caching method. However, it does provide consistent logic and will incoroporate any future enhancements. It can be used to create your own caching behavior. Here we show how we use the binding function within Caching as a basic example.

The bindCachingCore method is designed for use in a pre observer and the first two parameters are the url and init passed to pre. The third parameter is an optional Partial. It returns a tuple with three values. The first is a calculated value indicating if this request should be cached based on the internal default logic of the library, you can use this value in conjunction with your own logic. The second value is a function that will get a cached value, note no key is passed - the key is calculated and held within bindCachingCore. The third value is a function to which you pass a value to cache. The key and expiration are similarly calculated and held within bindCachingCore.

import { TimelinePipe } from \"@pnp/core\";\nimport { bindCachingCore, ICachingProps, Queryable } from \"@pnp/queryable\";\n\nexport function Caching(props?: ICachingProps): TimelinePipe<Queryable> {\n\n    return (instance: Queryable) => {\n\n        instance.on.pre(async function (this: Queryable, url: string, init: RequestInit, result: any): Promise<[string, RequestInit, any]> {\n\n            const [shouldCache, getCachedValue, setCachedValue] = bindCachingCore(url, init, props);\n\n            // only cache get requested data or where the CacheAlways header is present (allows caching of POST requests)\n            if (shouldCache) {\n\n                const cached = getCachedValue();\n\n                 // we need to ensure that result stays \"undefined\" unless we mean to set null as the result\n                if (cached === null) {\n\n                    // if we don't have a cached result we need to get it after the request is sent. Get the raw value (un-parsed) to store into cache\n                    this.on.rawData(noInherit(async function (response) {\n                        setCachedValue(response);\n                    }));\n\n                } else {\n                    // if we find it in cache, override send request, and continue flow through timeline and parsers.\n                    this.on.auth.clear();\n                    this.on.send.replace(async function (this: Queryable) {\n                        return new Response(cached, {});\n                    });\n                }\n            }\n\n            return [url, init, result];\n        });\n\n        return instance;\n    };\n}\n
"},{"location":"queryable/behaviors/#cachekey","title":"CacheKey","text":"

Added in 3.5.0

This behavior allows you to set a pre-determined cache key for a given request. It needs to be used PER request otherwise the value will be continuously overwritten.

import { Caching, CacheKey } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...).using(Caching());\n\n// note the application of the behavior on individual requests, if you share a CacheKey behavior across requests you'll encounter conflicts\nconst webInfo = await sp.web.using(CacheKey(\"MyWebInfoCacheKey\"))();\n\nconst listsInfo = await sp.web.lists.using(CacheKey(\"MyListsInfoCacheKey\"))();\n
"},{"location":"queryable/behaviors/#cachealways","title":"CacheAlways","text":"

Added in 3.8.0

This behavior allows you to force caching for a given request. This should not be used for update/create operations as the request will not execute if a result is found in the cache

import { Caching, CacheAlways } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...).using(Caching());\n\nconst webInfo = await sp.web.using(CacheAlways())();\n
"},{"location":"queryable/behaviors/#cachenever","title":"CacheNever","text":"

Added in 3.10.0

This behavior allows you to force skipping caching for a given request.

import { Caching, CacheNever } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...).using(Caching());\n\nconst webInfo = await sp.web.using(CacheNever())();\n
"},{"location":"queryable/behaviors/#caching-pessimistic-refresh","title":"Caching Pessimistic Refresh","text":"

This behavior is slightly different than our default Caching behavior in that it will always return the cached value if there is one, but also asyncronously update the cached value in the background. Like the default CAchine behavior it allows you to cache the results of get requests in either session or local storage. If neither is available (such as in Nodejs) the library will shim using an in memory map.

If you do not provide an expiration function then the cache will be updated asyncronously on every call, if you do provide an expiration then the cached value will only be updated, although still asyncronously, only when the cache has expired.

import { CachingPessimisticRefresh } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(CachingPessimisticRefresh());\n\n// caching will save the data into session storage on the first request - the key is based on the full url including query strings\nconst webInfo = await sp.webs();\n\n// caching will retriece this value from the cache saving a network requests the second time it is loaded (either in the same page, a reload of the page, etc.)\nconst webInfo2 = await sp.webs();\n

Again as with the default Caching behavior you can provide custom functions for key generation and expiration. Please see the Custom Key Function documentation above for more details.

"},{"location":"queryable/behaviors/#injectheaders","title":"InjectHeaders","text":"

Adds any specified headers to a given request. Can be used multiple times with a timeline. The supplied headers are added to all requests, and last applied wins - meaning if two InjectHeaders are included in the pipeline which inlcude a value for the same header, the second one applied will be used.

import { InjectHeaders } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(InjectHeaders({\n    \"X-Something\": \"a value\",\n    \"MyCompanySpecialAuth\": \"special company token\",\n}));\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#parsers","title":"Parsers","text":"

Parsers convert the returned fetch Response into something usable. We have included the most common parsers we think you'll need - but you can always write your own parser based on the signature of the parse moment.

All of these parsers when applied through using will replace any other observers on the parse moment.

"},{"location":"queryable/behaviors/#defaultparse","title":"DefaultParse","text":"

Performs error handling and parsing of JSON responses. This is the one you'll use for most of your requests and it is included in all the defaults.

import { DefaultParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(DefaultParse());\n\nconst webInfo = await sp.webs();\n
"},{"location":"queryable/behaviors/#textparse","title":"TextParse","text":"

Checks for errors and parses the results as text with no further manipulation.

import { TextParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(TextParse());\n
"},{"location":"queryable/behaviors/#blobparse","title":"BlobParse","text":"

Checks for errors and parses the results a Blob with no further manipulation.

import { BlobParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BlobParse());\n
"},{"location":"queryable/behaviors/#jsonparse","title":"JSONParse","text":"

Checks for errors and parses the results as JSON with no further manipulation. Meaning you will get the raw JSON response vs DefaultParse which will remove wrapping JSON.

import { JSONParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(JSONParse());\n
"},{"location":"queryable/behaviors/#bufferparse","title":"BufferParse","text":"

Checks for errors and parses the results a Buffer with no further manipulation.

import { BufferParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(BufferParse());\n
"},{"location":"queryable/behaviors/#headerparse","title":"HeaderParse","text":"

Checks for errors and parses the headers of the Response as the result. This is a specialised parses which can be used in those infrequent scenarios where you need information from the headers of a response.

import { HeaderParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(HeaderParse());\n
"},{"location":"queryable/behaviors/#jsonheaderparse","title":"JSONHeaderParse","text":"

Checks for errors and parses the headers of the Respnose as well as the JSON and returns an object with both values.

import { JSONHeaderParse } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(JSONHeaderParse());\n\n...sp.data\n...sp.headers\n
"},{"location":"queryable/behaviors/#resolvers","title":"Resolvers","text":"

These two behaviors are special and should always be included when composing your own defaults. They implement the expected behavior of resolving or rejecting the promise returned when executing a timeline. They are implemented as behaviors should there be a need to do something different the logic is not locked into the core of the library.

"},{"location":"queryable/behaviors/#resolveondata-rejectonerror","title":"ResolveOnData, RejectOnError","text":"
import { ResolveOnData, RejectOnError } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...).using(ResolveOnData(), RejectOnError());\n
"},{"location":"queryable/behaviors/#timeout","title":"Timeout","text":"

The Timeout behavior allows you to include a timeout in requests. You can specify either a number, representing the number of milliseconds until the request should timeout or an AbortSignal.

In Nodejs you will need to polyfill AbortController if your version (<15) does not include it when using Timeout and passing a number. If you are supplying your own AbortSignal you do not.

import { Timeout } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\n// requests should timeout in 5 seconds\nconst sp = spfi(...).using(Timeout(5000));\n
import { Timeout } from \"@pnp/queryable\";\n\nimport \"@pnp/sp/webs\";\n\nconst controller = new AbortController();\n\nconst sp = spfi(...).using(Timeout(controller.signal));\n\n// abort requests after 6 seconds using our own controller\nconst timer = setTimeout(() => {\n    controller.abort();\n}, 6000);\n\n// this request will be cancelled if it doesn't complete in 6 seconds\nconst webInfo = await sp.webs();\n\n// be a good citizen and cancel unneeded timers\nclearTimeout(timer);\n
"},{"location":"queryable/behaviors/#cancelable","title":"Cancelable","text":"

Updated as Beta 2 in 3.5.0

This behavior allows you to cancel requests before they are complete. It is similar to timeout however you control when and if the request is canceled. Please consider this behavior as beta while we work to stabalize the functionality.

"},{"location":"queryable/behaviors/#known-issues","title":"Known Issues","text":"
  • Due to how the event loop works you may get unhandled rejections after canceling a request
import { Cancelable, CancelablePromise } from \"@pnp/queryable\";\nimport { IWebInfo } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(Cancelable());\n\nconst p: CancelablePromise<IWebInfo> = <any>sp.web();\n\nsetTimeout(() => {\n\n    // you should await the cancel operation to ensure it completes\n    await p.cancel();\n}, 200);\n\n// this is awaiting the results of the request\nconst webInfo: IWebInfo = await p;\n
"},{"location":"queryable/behaviors/#cancel-long-running-operations","title":"Cancel long running operations","text":"

Some operations such as chunked uploads that take longer to complete are good candidates for canceling based on user input such as a button select.

import { Cancelable, CancelablePromise } from \"@pnp/queryable\";\nimport { IFileAddResult } from \"@pnp/sp/files\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport { getRandomString } from \"@pnp/core\";\nimport { createReadStream } from \"fs\";\n\nconst sp = spfi().using(Cancelable());\n\nconst file = createReadStream(join(\"C:/some/path\", \"test.mp4\"));\n\nconst p: CancelablePromise<IFileAddResult> = <any>sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents\").files.addChunked(`te's't-${getRandomString(4)}.mp4`, <any>file);\n\nsetTimeout(() => {\n\n    // you should await the cancel operation to ensure it completes\n    await p.cancel();\n}, 10000);\n\n// this is awaiting the results of the request\nawait p;\n
"},{"location":"queryable/extensions/","title":"Extensions","text":"

Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invokable and allow you to control any behavior of the library with extensions.

"},{"location":"queryable/extensions/#types-of-extensions","title":"Types of Extensions","text":"

There are two types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.

"},{"location":"queryable/extensions/#function-extensions","title":"Function Extensions","text":"

The first type is a simple function with a signature:

(op: \"apply\" | \"get\" | \"has\" | \"set\", target: T, ...rest: any[]): void\n

This function is passed the current operation as the first argument, currently one of \"apply\", \"get\", \"has\", or \"set\". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.

"},{"location":"queryable/extensions/#named-extensions","title":"Named Extensions","text":"

Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables.

import { extendFactory } from \"@pnp/queryable\";\nimport { sp, List, Lists, IWeb, ILists, List, IList, Web } from \"@pnp/sp/presets/all\";\nimport { escapeQueryStrValue } from \"@pnp/sp/utils/escapeQueryStrValue\";\n\n// create a plain object with the props and methods we want to add/change\nconst myExtensions = {\n    // override the lists property\n    get lists(this: IWeb): ILists {\n        // we will always order our lists by title and select just the Title for ALL calls (just as an example)\n        return Lists(this).orderBy(\"Title\").select(\"Title\");\n    },\n    // override the getByTitle method\n    getByTitle: function (this: ILists, title: string): IList {\n        // in our example our list has moved, so we rewrite the request on the fly\n        if (title === \"List1\") {\n            return List(this, `getByTitle('List2')`);\n        } else {\n            // you can't at this point call the \"base\" method as you will end up in loop within the proxy\n            // so you need to ensure you patch/include any original functionality you need\n            return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);\n        }\n    },\n};\n\n// register all the named Extensions\nextendFactory(Web, myExtensions);\n\n// this will use our extension to ensure the lists are ordered\nconst lists = await sp.web.lists();\n\nconsole.log(JSON.stringify(lists, null, 2));\n\n// we will get the items from List1 but within the extension it is rewritten as List2\nconst items = await sp.web.lists.getByTitle(\"List1\").items();\n\nconsole.log(JSON.stringify(items.length, null, 2));\n
"},{"location":"queryable/extensions/#proxyhandler-extensions","title":"ProxyHandler Extensions","text":"

You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work.

import { extendFactory } from \"@pnp/queryable\";\nimport { sp, Lists, IWeb, ILists, Web } from \"@pnp/sp/presets/all\";\nimport { escapeQueryStrValue } from \"@pnp/sp/utils/escapeSingleQuote\";\n\nconst myExtensions = {\n    get: (target, p: string | number | symbol, _receiver: any) => {\n        switch (p) {\n            case \"getByTitle\":\n                return (title: string) => {\n\n                    // in our example our list has moved, so we rewrite the request on the fly\n                    if (title === \"LookupList\") {\n                        return List(target, `getByTitle('OrderByList')`);\n                    } else {\n                        // you can't at this point call the \"base\" method as you will end up in loop within the proxy\n                        // so you need to ensure you patch/include any original functionality you need\n                        return List(target, `getByTitle('${escapeQueryStrValue(title)}')`);\n                    }\n                };\n        }\n    },\n};\n\nextendFactory(Web, myExtensions);\n\nconst lists = sp.web.lists;\nconst items = await lists.getByTitle(\"LookupList\").items();\n\nconsole.log(JSON.stringify(items.length, null, 2));\n
"},{"location":"queryable/extensions/#registering-extensions","title":"Registering Extensions","text":"

You can register Extensions on an invocable factory or on a per-object basis, and you can register a single extension or an array of Extensions.

"},{"location":"queryable/extensions/#factory-registration","title":"Factory Registration","text":"

The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { IWeb, Web } from \"@pnp/sp/webs\";\nimport { ILists, Lists } from \"@pnp/sp/lists\";\nimport { extendFactory } from \"@pnp/queryable\";\nimport { sp } from \"@pnp/sp\";\n\nconst sp = spfi().using(...);\n\n// sets up the types correctly when importing across your application\ndeclare module \"@pnp/sp/webs/types\" {\n\n    // we need to extend the interface\n    interface IWeb {\n        orderedLists: ILists;\n    }\n}\n\n// sets up the types correctly when importing across your application\ndeclare module \"@pnp/sp/lists/types\" {\n\n    // we need to extend the interface\n    interface ILists {\n        getOrderedListsQuery: (this: ILists) => ILists;\n    }\n}\n\nextendFactory(Web, {\n    // add an ordered lists property\n    get orderedLists(this: IWeb): ILists {\n        return this.lists.getOrderedListsQuery();\n    },\n});\n\nextendFactory(Lists, {\n    // add an ordered lists property\n    getOrderedListsQuery(this: ILists): ILists {\n        return this.top(10).orderBy(\"Title\").select(\"Title\");\n    },\n});\n\n// regardless of how we access the web and lists collections our extensions remain with all new instance based on\nconst web = Web([sp.web, \"https://tenant.sharepoint.com/sites/dev/\"]);\nconst lists1 = await web.orderedLists();\nconsole.log(JSON.stringify(lists1, null, 2));\n\nconst lists2 = await Web([sp.web, \"https://tenant.sharepoint.com/sites/dev/\"]).orderedLists();\nconsole.log(JSON.stringify(lists2, null, 2));\n\nconst lists3 = await sp.web.orderedLists();\nconsole.log(JSON.stringify(lists3, null, 2));\n
"},{"location":"queryable/extensions/#instance-registration","title":"Instance Registration","text":"

You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances.

Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are.

Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance.

import { extendObj } from \"@pnp/queryable\";\nimport { sp, List, ILists } from \"@pnp/sp/presets/all\";\n\nconst myExtensions = {\n    getByTitle: function (this: ILists, title: string) {\n        // in our example our list has moved, so we rewrite the request on the fly\n        if (title === \"List1\") {\n            return List(this, \"getByTitle('List2')\");\n        } else {\n            // you can't at this point call the \"base\" method as you will end up in loop within the proxy\n            // so you need to ensure you patch/include any original functionality you need\n            return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);\n        }\n    },\n};\n\nconst lists =  extendObj(sp.web.lists, myExtensions);\nconst items = await lists.getByTitle(\"LookupList\").items();\n\nconsole.log(JSON.stringify(items.length, null, 2));\n
"},{"location":"queryable/extensions/#enable-disable-extensions-and-clear-global-extensions","title":"Enable & Disable Extensions and Clear Global Extensions","text":"

Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed.

import { enableExtensions, disableExtensions, clearGlobalExtensions } from \"@pnp/queryable\";\n\n// disable Extensions\ndisableExtensions();\n\n// enable Extensions\nenableExtensions();\n
"},{"location":"queryable/queryable/","title":"@pnp/queryable/queryable","text":"

Queryable is the base class for both the sp and graph fluent interfaces and provides the structure to which observers are registered. As a background to understand more of the mechanics please see the articles on Timeline, moments, and observers. For reuse it is recommended to compose your observer registrations with behaviors.

"},{"location":"queryable/queryable/#queryable-constructor","title":"Queryable Constructor","text":"

By design the library is meant to allow creating the next part of a url from the current part. In this way each queryable instance is built from a previous instance. As such understanding the Queryable constructor's behavior is important. The constructor takes two parameters, the first required and the second optional.

The first parameter can be another queryable, a string, or a tuple of [Queryable, string].

Parameter Behavior Queryable The new queryable inherits all of the supplied queryable's observers. Any supplied path (second constructor param) is appended to the supplied queryable's url becoming the url of the newly constructed queryable string The new queryable will have NO registered observers. Any supplied path (second constructor param) is appended to the string becoming the url of the newly constructed queryable [Queryable, string] The observers from the supplied queryable are used by the new queryable. The url is a combination of the second tuple argument (absolute url string) and any supplied path.

The tuple constructor call can be used to rebase a queryable to call a different host in an otherwise identical way to another queryable. When using the tuple constructor the url provided must be absolute.

"},{"location":"queryable/queryable/#examples","title":"Examples","text":"
// represents a fully configured queryable with url and registered observers\n// url: https://something.com\nconst baseQueryable;\n\n// child1 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://something.com/subpath\"\nconst child1 = Child(baseQueryable, \"subpath\");\n\n// child2 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://something.com\"\nconst child2 = Child(baseQueryable);\n\n// nonchild1 will:\n// - have NO registered observers or connection to baseQueryable\n// - have a url of \"https://somethingelse.com\"\nconst nonchild1 = Child(\"https://somethingelse.com\");\n\n// nonchild2 will:\n// - have NO registered observers or connection to baseQueryable\n// - have a url of \"https://somethingelse.com/subpath\"\nconst nonchild2 = Child(\"https://somethingelse.com\", \"subpath\");\n\n// rebased1 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://somethingelse.com\"\nconst rebased1 = Child([baseQueryable, \"https://somethingelse.com\"]);\n\n// rebased2 will:\n// - reference the observers of baseQueryable\n// - have a url of \"https://somethingelse.com/subpath\"\nconst rebased2 = Child([baseQueryable, \"https://somethingelse.com\"], \"subpath\");\n
"},{"location":"queryable/queryable/#queryable-lifecycle","title":"Queryable Lifecycle","text":"

The Queryable lifecycle is:

  • construct (Added in 3.5.0)
  • init
  • pre
  • auth
  • send
  • parse
  • post
  • data
  • dispose

As well log and error can emit at any point during the lifecycle.

"},{"location":"queryable/queryable/#no-observers-registered-for-this-request","title":"No observers registered for this request","text":"

If you see an error thrown with the message No observers registered for this request. it means at the time of execution the given object has no actions to take. Because all the request logic is defined within observers, an absence of observers is likely an error condition. If the object was created by a method within the library please report an issue as it is likely a bug. If you created the object through direct use of one of the factory functions, please be sure you have registered observers with using or on as appropriate. More information on observers is available in this article.

If you for some reason want to execute a queryable with no registred observers, you can simply register a noop observer to any of the moments.

"},{"location":"queryable/queryable/#queryable-observers","title":"Queryable Observers","text":"

This section outlines how to write observers for the Queryable lifecycle, and the expectations of each moment's observer behaviors.

In the below samples consider the variable query to mean any valid Queryable derived object.

"},{"location":"queryable/queryable/#log","title":"log","text":"

Anything can log to a given timeline's log using the public log method and to intercept those message you can subscribed to the log event.

The log observer's signature is: (this: Timeline<T>, message: string, level: number) => void

query.on.log((message, level) => {\n\n    // log only warnings or errors\n    if (level > 1) {\n        console.log(message);\n    }\n});\n

The level value is a number indicating the severity of the message. Internally we use the values from the LogLevel enum in @pnp/logging: Verbose = 0, Info = 1, Warning = 2, Error = 3. Be aware that nothing enforces those values other than convention and log can be called with any value for level.

As well we provide easy support to use PnP logging within a Timeline derived class:

import { LogLevel, PnPLogging } from \"@pnp/logging\";\n\n// any messages of LogLevel Info or higher (1) will be logged to all subscribers of the logging framework\nquery.using(PnPLogging(LogLevel.Info));\n

More details on the pnp logging framework

"},{"location":"queryable/queryable/#error","title":"error","text":"

Errors can happen at anytime and for any reason. If you are using the RejectOnError behavior, and both sp and graph include that in the defaults, the request promise will be rejected as expected and you can handle the error that way.

The error observer's signature is: (this: Timeline<T>, err: string | Error) => void

import { spfi, DefaultInit, DefaultHeaders } from \"@pnp/sp\";\nimport { BrowserFetchWithRetry, DefaultParse } from \"@pnp/queryable\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(DefaultInit(), DefaultHeaders(), BrowserFetchWithRetry(), DefaultParse());\n\ntry {\n\n    const result = await sp.web();\n\n} catch(e) {\n\n    // any errors emitted will result in the promise being rejected\n    // and ending up in the catch block as expected\n}\n

In addition to the default behavior you can register your own observers on error, though it is recommended you leave the default behavior in place.

query.on.error((err) => {\n\n    if (err) {\n        console.error(err);\n        // do other stuff with the error (send it to telemetry)\n    }\n});\n
"},{"location":"queryable/queryable/#construct","title":"construct","text":"

Added in 3.5.0

This moment exists to assist behaviors that need to transfer some information from a parent to a child through the fluent chain. We added this to support cancelable scopes for the Cancelable behavior, but it may have other uses. It is invoked AFTER the new instance is fully realized via new and supplied with the parameters used to create the new instance. As with all moments the \"this\" within the observer is the current (NEW) instance.

For your observers on the construct method to work correctly they must be registered before the instance is created.

The construct moment is NOT async and is designed to support simple operations.

query.on.construct(function (this: Queryable, init: QueryableInit, path?: string): void {\n    if (typeof init !== \"string\") {\n\n        // get a ref to the parent Queryable instance used to create this new instance\n        const parent = isArray(init) ? init[0] : init;\n\n        if (Reflect.has(parent, \"SomeSpecialValueKey\")) {\n\n            // copy that specail value to the new child\n            this[\"SomeSpecialValueKey\"] = parent[\"SomeSpecialValueKey\"];\n        }\n    }     \n});\n\nquery.on.pre(async function(url, init, result) {\n\n    // we have access to the copied special value throughout the lifecycle\n    this.log(this[\"SomeSpecialValueKey\"]);\n\n    return [url, init, result];\n});\n\nquery.on.dispose(() => {\n\n    // be a good citizen and clean up your behavior's values when you're done\n    delete this[\"SomeSpecialValueKey\"];\n});\n
"},{"location":"queryable/queryable/#init","title":"init","text":"

Along with dispose, init is a special moment that occurs before any of the other lifecycle providing a first chance at doing any tasks before the rest of the lifecycle starts. It is not await aware so only sync operations are supported in init by design.

The init observer's signature is: (this: Timeline<T>) => void

In the case of init you manipulate the Timeline instance itself

query.on.init(function (this: Queryable) {\n\n    // init is a great place to register additioanl observers ahead of the lifecycle\n    this.on.pre(async function (this: Quyerable, url, init, result) {\n        // stuff happens\n        return [url, init, result];\n    });\n});\n
"},{"location":"queryable/queryable/#pre","title":"pre","text":"

Pre is used by observers to configure the request before sending. Note there is a dedicated auth moment which is prefered by convention to handle auth related tasks.

The pre observer's signature is: (this: IQueryable, url: string, init: RequestInit, result: any) => Promise<[string, RequestInit, any]>

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

Example of when to use pre are updates to the init, caching scenarios, or manipulation of the url (ensuring it is absolute). The init passed to pre (and auth) is the same object that will be eventually passed to fetch, meaning you can add any properties/congifuration you need. The result should always be left undefined unless you intend to end the lifecycle. If pre completes and result has any value other than undefined that value will be emitted to data and the timeline lifecycle will end.

query.on.pre(async function(url, init, result) {\n\n    init.cache = \"no-store\";\n\n    return [url, init, result];\n});\n\nquery.on.pre(async function(url, init, result) {\n\n    // setting result causes no moments after pre to be emitted other than data\n    // once data is emitted (resolving the request promise by default) the lifecycle ends\n    result = \"My result\";\n\n    return [url, init, result];\n});\n
"},{"location":"queryable/queryable/#auth","title":"auth","text":"

Auth functions very much like pre except it does not have the option to set the result, and the url is considered immutable by convention. Url manipulation should be done in pre. Having a seperate moment for auth allows for easily changing auth specific behavior without having to so a lot of complicated parsing of pre observers.

The auth observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<[URL, RequestInit]>.

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

query.on.auth(async function(url, init) {\n\n    // some code to get a token\n    const token = getToken();\n\n    init.headers[\"Authorization\"] = `Bearer ${token}`;\n\n    return [url, init];\n});\n
"},{"location":"queryable/queryable/#send","title":"send","text":"

Send is implemented using the request moment which uses the first registered observer and invokes it expecting an async Response.

The send observer's signature is: (this: IQueryable, url: URL, init: RequestInit) => Promise<Response>.

query.on.send(async function(url, init) {\n\n    // this could represent reading a file, querying a database, or making a web call\n    return fetch(url.toString(), init);\n});\n
"},{"location":"queryable/queryable/#parse","title":"parse","text":"

Parse is responsible for turning the raw Response into something usable. By default we handle errors and parse JSON responses, but any logic could be injected here. Perhaps your company encrypts things and you need to decrypt them before parsing further.

The parse observer's signature is: (this: IQueryable, url: URL, response: Response, result: any | undefined) => Promise<[URL, Response, any]>.

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

// you should be careful running multiple parse observers so we replace with our functionality\n// remember every registered observer is run, so if you set result and a later observer sets a\n// different value last in wins.\nquery.on.parse.replace(async function(url, response, result) {\n\n    if (response.ok) {\n\n        result = await response.json();\n\n    } else {\n\n        // just an example\n        throw Error(response.statusText);\n    }\n\n    return [url, response, result];\n});\n
"},{"location":"queryable/queryable/#post","title":"post","text":"

Post is run after parse, meaning you should have a valid fully parsed result, and provides a final opportunity to do caching, some final checks, or whatever you might need immediately prior to the request promise resolving with the value. It is recommened to NOT manipulate the result within post though nothing prevents you from doing so.

The post observer's signature is: (this: IQueryable, url: URL, result: any | undefined) => Promise<[URL, any]>.

The pre, auth, parse, and post are asyncReduce moments, meaning you are expected to always asyncronously return a tuple of the arguments supplied to the function. These are then passed to the next observer registered to the moment.

query.on.post(async function(url, result) {\n\n    // here we do some caching of a result\n    const key = hash(url);\n    cache(key, result);   \n\n    return [url, result];\n});\n
"},{"location":"queryable/queryable/#data","title":"data","text":"

Data is called with the result of the Queryable lifecycle produced by send, understood by parse, and passed through post. By default the request promise will resolve with the value, but you can add any additional observers you need.

The data observer's signature is: (this: IQueryable, result: T) => void.

Clearing the data moment (ie. .on.data.clear()) after the lifecycle has started will result in the request promise never resolving

query.on.data(function(result) {\n\n    console.log(`Our result! ${JSON.stringify(result)}`);\n});\n
"},{"location":"queryable/queryable/#dispose","title":"dispose","text":"

Along with init, dispose is a special moment that occurs after all other lifecycle moments have completed. It is not await aware so only sync operations are supported in dispose by design.

The dispose observer's signature is: (this: Timeline<T>) => void

In the case of dispose you manipulate the Timeline instance itself

query.on.dispose(function (this: Queryable) {\n\n    // maybe your queryable calls a database?\n    db.connection.close();\n});\n
"},{"location":"queryable/queryable/#other-methods","title":"Other Methods","text":"

Queryable exposes some additional methods beyond the observer registration.

"},{"location":"queryable/queryable/#concat","title":"concat","text":"

Appends the supplied string to the url without mormalizing slashes.

// url: something.com/items\nquery.concat(\"(ID)\");\n// url: something.com/items(ID)\n
"},{"location":"queryable/queryable/#torequesturl","title":"toRequestUrl","text":"

Converts the queryable's internal url parameters (url and query) into a relative or absolute url.

const s = query.toRequestUrl();\n
"},{"location":"queryable/queryable/#query","title":"query","text":"

Map used to manage any query string parameters that will be included. Anything added here will be represented in toRequestUrl's output.

query.query.add(\"$select\", \"Title\");\n
"},{"location":"queryable/queryable/#tourl","title":"toUrl","text":"

Returns the url currently represented by the Queryable, without the querystring part

const s = query.toUrl();\n
"},{"location":"sp/alias-parameters/","title":"@pnp/sp - Aliased Parameters","text":"

Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders.

To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query.

"},{"location":"sp/alias-parameters/#construct-a-parameter-alias","title":"Construct a parameter alias","text":"

Pattern: !@{label name}::{value}

Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\"

"},{"location":"sp/alias-parameters/#example-without-aliasing","title":"Example without aliasing","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nconst sp = spfi(...);\n\n// still works as expected, no aliasing\nconst query = sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/\").files.select(\"Title\").top(3);\n\nconsole.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files\nconsole.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3\n\nconst r = await query();\nconsole.log(r);\n
"},{"location":"sp/alias-parameters/#example-with-aliasing","title":"Example with aliasing","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// same query with aliasing\nconst query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3);\n\nconsole.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files\nconsole.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3\n\nconst r = await query();\nconsole.log(r);\n
"},{"location":"sp/alias-parameters/#example-with-aliasing-and-batching","title":"Example with aliasing and batching","text":"

Aliasing is supported with batching as well:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// same query with aliasing and batching\nconst [batchedWeb, execute] = await sp.web.batched();\n\nconst query = batchedWeb.web.getFolderByServerRelativePath(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3);\n\nconsole.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files\nconsole.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3\n\nquery().then(r => {\n\n    console.log(r);\n});\n\nexecute();\n
"},{"location":"sp/alm/","title":"@pnp/sp/appcatalog","text":"

The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.

"},{"location":"sp/alm/#understanding-the-app-catalog-hierarchy","title":"Understanding the App Catalog Hierarchy","text":"

Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.

"},{"location":"sp/alm/#referencing-an-app-catalog","title":"Referencing an App Catalog","text":"

There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.

"},{"location":"sp/alm/#get-tenant-app-catalog","title":"Get tenant app catalog","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// get the current context web's app catalog\n// this will be the site collection app catalog\nconst availableApps = await sp.tenantAppcatalog();\n
"},{"location":"sp/alm/#get-site-collection-appcatalog","title":"Get site collection AppCatalog","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// get the current context web's app catalog\nconst availableApps = await sp.web.appcatalog();\n
"},{"location":"sp/alm/#get-site-collection-appcatalog-by-url","title":"Get site collection AppCatalog by URL","text":"

If you know the url of the site collection whose app catalog you want you can use the following code. First you need to use one of the methods to access a web. Once you have the web instance you can call the .appcatalog property on that web instance.

If a given site collection does not have an app catalog trying to access it will throw an error.

import { spfi } from \"@pnp/sp\";\nimport { Web } from '@pnp/sp/webs';\n\nconst sp = spfi(...);\nconst web = Web([sp.web, \"https://mytenant.sharepoint.com/sites/mysite\"]);\nconst catalog = await web.appcatalog();\n

The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.

"},{"location":"sp/alm/#list-available-apps","title":"List Available Apps","text":"

The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.

// get available apps\nawait catalog();\n\n// get available apps selecting two fields\nawait catalog.select(\"Title\", \"Deployed\")();\n
"},{"location":"sp/alm/#add-an-app","title":"Add an App","text":"

This action must be performed in the context of the tenant app catalog

// this represents the file bytes of the app package file\nconst blob = new Blob();\n\n// there is an optional third argument to control overwriting existing files\nconst r = await catalog.add(\"myapp.app\", blob);\n\n// this is at its core a file add operation so you have access to the response data as well\n// as a File instance representing the created file\nconsole.log(JSON.stringify(r.data, null, 4));\n\n// all file operations are available\nconst nameData = await r.file.select(\"Name\")();\n
"},{"location":"sp/alm/#get-an-app","title":"Get an App","text":"

You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions

const app = await catalog.getAppById(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\")();\n
"},{"location":"sp/alm/#perform-app-actions","title":"Perform app actions","text":"

Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block.

const myAppId = \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\";\n\n// deploy\nawait catalog.getAppById(myAppId).deploy();\n\n// retract\nawait catalog.getAppById(myAppId).retract();\n\n// install\nawait catalog.getAppById(myAppId).install();\n\n// uninstall\nawait catalog.getAppById(myAppId).uninstall();\n\n// upgrade\nawait catalog.getAppById(myAppId).upgrade();\n\n// remove\nawait catalog.getAppById(myAppId).remove();\n\n
"},{"location":"sp/alm/#synchronize-a-solutionapp-to-the-microsoft-teams-app-catalog","title":"Synchronize a solution/app to the Microsoft Teams App Catalog","text":"

By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id.

// Using the app id\nawait catalog.syncSolutionToTeams(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\");\n\n// Using the SharePoint apps item id\nawait catalog.syncSolutionToTeams(\"123\", true);\n
"},{"location":"sp/alm/#notes","title":"Notes","text":"
  • The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.
"},{"location":"sp/attachments/","title":"@pnp/sp/attachments","text":"

The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.

"},{"location":"sp/attachments/#get-attachments","title":"Get attachments","text":"
import { spfi } from \"@pnp/sp\";\nimport { IAttachmentInfo } from \"@pnp/sp/attachments\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\n// get all the attachments\nconst info: IAttachmentInfo[] = await item.attachmentFiles();\n\n// get a single file by file name\nconst info2: IAttachmentInfo = await item.attachmentFiles.getByName(\"file.txt\")();\n\n// select specific properties using odata operators and use Pick to type the result\nconst info3: Pick<IAttachmentInfo, \"ServerRelativeUrl\">[] = await item.attachmentFiles.select(\"ServerRelativeUrl\")();\n
"},{"location":"sp/attachments/#add-an-attachment","title":"Add an Attachment","text":"

You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.

import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.add(\"file2.txt\", \"Here is my content\");\n
"},{"location":"sp/attachments/#read-attachment-content","title":"Read Attachment Content","text":"

You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.

import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nconst text = await item.attachmentFiles.getByName(\"file.txt\").getText();\n\n// use this in the browser, does not work in nodejs\nconst blob = await item.attachmentFiles.getByName(\"file.mp4\").getBlob();\n\n// use this in nodejs\nconst buffer = await item.attachmentFiles.getByName(\"file.mp4\").getBuffer();\n\n// file must be valid json\nconst json = await item.attachmentFiles.getByName(\"file.json\").getJSON();\n
"},{"location":"sp/attachments/#update-attachment-content","title":"Update Attachment Content","text":"

You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library.

import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.getByName(\"file2.txt\").setContent(\"My new content!!!\");\n
"},{"location":"sp/attachments/#delete-attachment","title":"Delete Attachment","text":"
import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.getByName(\"file2.txt\").delete();\n
"},{"location":"sp/attachments/#recycle-attachment","title":"Recycle Attachment","text":"

Delete the attachment and send it to recycle bin

import { spfi } from \"@pnp/sp\";\nimport { IItem } from \"@pnp/sp/items/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1);\n\nawait item.attachmentFiles.getByName(\"file2.txt\").recycle();\n
"},{"location":"sp/attachments/#recycle-multiple-attachments","title":"Recycle Multiple Attachments","text":"

Delete multiple attachments and send them to recycle bin

import { spfi } from \"@pnp/sp\";\nimport { IList } from \"@pnp/sp/lists/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/attachments\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nconst item = await batchedSP.web.lists.getByTitle(\"MyList\").items.getById(2);\n\nitem.attachmentFiles.getByName(\"1.txt\").recycle();\nitem.attachmentFiles.getByName(\"2.txt\").recycle();\n\nawait execute();\n
"},{"location":"sp/behaviors/","title":"@pnp/sp : behaviors","text":"

The article describes the behaviors exported by the @pnp/sp library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/graph, and @pnp/nodejs.

"},{"location":"sp/behaviors/#defaultinit","title":"DefaultInit","text":"

The DefaultInit behavior, is a composed behavior which includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit.

import { spfi, DefaultInit } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(DefaultInit());\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#defaultheaders","title":"DefaultHeaders","text":"

The DefaultHeaders behavior uses InjectHeaders to set the Accept, Content-Type, and User-Agent headers.

import { spfi, DefaultHeaders } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(DefaultHeaders());\n\nawait sp.web();\n

DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.

"},{"location":"sp/behaviors/#requestdigest","title":"RequestDigest","text":"

The RequestDigest behavior ensures that the \"X-RequestDigest\" header is included for requests where it is needed. If you are using MSAL, supplying your own tokens, or doing a GET request it is not required. As well it cache's the digests to reduce the number of requests.

Optionally you can provide a function to supply your own digests. The logic followed by the behavior is to check the cache, run a hook if provided, and finally make a request to \"/_api/contextinfo\" for the value.

import { spfi, RequestDigest } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(RequestDigest());\n\nawait sp.web();\n

With a hook:

import { dateAdd } from \"@pnp/core\";\nimport { spfi, RequestDigest } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(RequestDigest((url, init) => {\n\n    // the url will be a URL instance representing the request url\n    // init will be the RequestInit\n\n    return {\n        expiration: dateAdd(new Date(), \"minute\", 20);\n        value: \"MY VALID REQUEST DIGEST VALUE\";\n    }\n}));\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#spbrowser","title":"SPBrowser","text":"

A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.

The baseUrl prop can be used to configure a fallback when making urls absolute.

If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.

You should set a baseUrl as shown below.

import { spfi, SPBrowser } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// you should use the baseUrl value when working in a SPA to ensure it is always properly set for all requests\nconst sp = spfi().using(SPBrowser({ baseUrl: \"https://tenant.sharepoint.com/sites/dev\" }));\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#spfx","title":"SPFx","text":"

This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. A hook is supplied to RequestDigest that will attempt to use any existing legacyPageContext formDigestValue it can find, otherwise defaults to the base RequestDigest behavior. It also sets a pre handler to ensure the url is absolute, using the SPFx context's pageContext.web.absoluteUrl as the base.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst sp = spfi(...).using(SPFx(this.context));\n\nawait sp.web();\n

Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.

import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'\nimport { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'\n\nconst sp = spfi().using(spSPFx(this.context));\nconst graph = graphfi().using(graphSPFx(this.context));\n
"},{"location":"sp/behaviors/#spfxtoken","title":"SPFxToken","text":"

Added in 3.12

Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\n// this.context represents the context object within an SPFx webpart, application customizer, or ACE.\nconst sp = spfi(...).using(SPFxToken(this.context));\n\nawait sp.web();\n
"},{"location":"sp/behaviors/#telemetry","title":"Telemetry","text":"

This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.

You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.

import { spfi, Telemetry } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi().using(Telemetry());\n\nawait sp.web();\n
"},{"location":"sp/clientside-pages/","title":"@pnp/sp/clientside-pages","text":"

The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts.

"},{"location":"sp/clientside-pages/#create-a-new-page","title":"Create a new Page","text":"

You can create a new client-side page in several ways, all are equivalent.

"},{"location":"sp/clientside-pages/#create-using-iwebaddclientsidepage","title":"Create using IWeb.addClientsidePage","text":"
import { spfi, SPFI } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\nimport { PromotedState } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// Create a page providing a file name\nconst page = await sp.web.addClientsidePage(\"mypage1\");\n\n// ... other operations on the page as outlined below\n\n// the page is initially not published, you must publish it so it appears for others users\nawait page.save();\n\n// include title and page layout\nconst page2 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\");\n\n// you must publish the new page\nawait page2.save();\n\n// include title, page layout, and specifying the publishing status (Added in 2.0.4)\nconst page3 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish);\n\n// you must publish the new page, after which the page will immediately be promoted to a news article\nawait page3.save();\n
"},{"location":"sp/clientside-pages/#create-using-createclientsidepage-method","title":"Create using CreateClientsidePage method","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { Web } from \"@pnp/sp/webs\";\nimport { CreateClientsidePage, PromotedState } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page1 = await CreateClientsidePage(sp.web, \"mypage2\", \"My Page Title\");\n\n// you must publish the new page\nawait page1.save(true);\n\n// specify the page layout type parameter\nconst page2 = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\");\n\n// you must publish the new page\nawait page2.save();\n\n// specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4)\nconst page2half = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish);\n\n// you must publish the new page, after which the page will immediately be promoted to a news article\nawait page2half.save();\n\n// use the web factory to create a page in a specific web\nconst page3 = await CreateClientsidePage(Web([sp, \"https://{absolute web url}\"]), \"mypage4\", \"My Page Title\");\n\n// you must publish the new page\nawait page3.save();\n
"},{"location":"sp/clientside-pages/#create-using-iwebaddfullpageapp","title":"Create using IWeb.addFullPageApp","text":"

Using this method you can easily create a full page app page given the component id. Don't forget the page will not be published and you will need to call save.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page = await sp.web.addFullPageApp(\"name333\", \"My Title\", \"2CE4E250-B997-11EB-A9D2-C9D2FF95D000\");\n// ... other page actions\n// you must save the page to publish it\nawait page.save();\n
"},{"location":"sp/clientside-pages/#load-pages","title":"Load Pages","text":"

There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.

"},{"location":"sp/clientside-pages/#load-using-iwebloadclientsidepage","title":"Load using IWeb.loadClientsidePage","text":"

This method takes a server relative path to the page to load.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { Web } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\n\nconst sp = spfi(...);\n\n// use from the sp.web fluent chain\nconst page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");\n\n// use the web factory to target a specific web\nconst page2 = await Web([sp.web, \"https://{absolute web url}\"]).loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");\n
"},{"location":"sp/clientside-pages/#load-using-clientsidepagefromfile","title":"Load using ClientsidePageFromFile","text":"

This method takes an IFile instance and loads an IClientsidePage instance.

import { spfi } from \"@pnp/sp\";\nimport { ClientsidePageFromFile } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\n\nconst sp = spfi(...);\n\nconst page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath(\"/sites/dev/sitepages/mypage3.aspx\"));\n
"},{"location":"sp/clientside-pages/#edit-sections-and-columns","title":"Edit Sections and Columns","text":"

Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12\nconst section1 = page.addSection();\nsection1.addColumn(6);\nsection1.addColumn(6);\n\n// create a three column layout in a new section\nconst section2 = page.addSection();\nsection2.addColumn(4);\nsection2.addColumn(4);\nsection2.addColumn(4);\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#manipulate-sections-and-columns","title":"Manipulate Sections and Columns","text":"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// drop all the columns in this section\n// this will also DELETE all controls contained in the columns\npage.sections[1].columns.length = 0;\n\n// create a new column layout\npage.sections[1].addColumn(4);\npage.sections[1].addColumn(8);\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#vertical-section","title":"Vertical Section","text":"

The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// add or get a vertical section (handles case where section already exists)\nconst vertSection = page.addVerticalSection();\n\n// ****************************************************************\n\n// if you know or want to test if a vertical section is present:\nif (page.hasVerticalSection) {\n\n    // access the vertical section (this method will NOT create the section if it does not exist)\n    page.verticalSection.addControl(new ClientsideText(\"hello\"));\n} else {\n\n    const vertSection = page.addVerticalSection();\n    vertSection.addControl(new ClientsideText(\"hello\"));\n}\n
"},{"location":"sp/clientside-pages/#reorder-sections","title":"Reorder Sections","text":"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// swap the order of two sections\n// this will preserve the controls within the columns\npage.sections = [page.sections[1], page.sections[0]];\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#reorder-columns","title":"Reorder Columns","text":"

The sections and columns are arrays, so normal array operations work as expected

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// swap the order of two columns\n// this will preserve the controls within the columns\npage.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]];\n\n// publish our changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#clientside-controls","title":"Clientside Controls","text":"

Once you have your sections and columns defined you will want to add/edit controls within those columns.

"},{"location":"sp/clientside-pages/#add-text-content","title":"Add Text Content","text":"
import { spfi } from \"@pnp/sp\";\nimport { ClientsideText, IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\npage.addSection().addControl(new ClientsideText(\"@pnp/sp is a great library!\"));\n\nawait page.save();\n
"},{"location":"sp/clientside-pages/#add-controls","title":"Add Controls","text":"

Adding controls involves loading the available client-side part definitions from the server or creating a text part.

import \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\nimport { spfi } from \"@pnp/sp\";\nimport { ClientsideWebpart } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// this will be a ClientsidePageComponent array\n// this can be cached on the client in production scenarios\nconst partDefs = await sp.web.getClientsideWebParts();\n\n// find the definition we want, here by id\nconst partDef = partDefs.filter(c => c.Id === \"490d7c76-1824-45b2-9de3-676421c997fa\");\n\n// optionally ensure you found the def\nif (partDef.length < 1) {\n    // we didn't find it so we throw an error\n    throw new Error(\"Could not find the web part\");\n}\n\n// create a ClientWebPart instance from the definition\nconst part = ClientsideWebpart.fromComponentDef(partDef[0]);\n\n// set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video.\n// the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting\n// the properties correctly\npart.setProperties<{ embedCode: string }>({\n    embedCode: \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\",\n});\n\n// we add that part to a new section\npage.addSection().addControl(part);\n\nawait page.save();\n
"},{"location":"sp/clientside-pages/#handle-different-webparts-settings","title":"Handle Different Webpart's Settings","text":"

There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title.

import { spfi } from \"@pnp/sp\";\nimport { ClientsideWebpart } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\n\n// we create a class to wrap our functionality in a reusable way\nclass ListWebpart extends ClientsideWebpart {\n\n  constructor(control: ClientsideWebpart) {\n    super((<any>control).json);\n  }\n\n  // add property getter/setter for what we need, in this case \"listTitle\" within searchablePlainTexts\n  public get DisplayTitle(): string {\n    return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || \"\";\n  }\n\n  public set DisplayTitle(value: string) {\n    this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value;\n  }\n}\n\nconst sp = spfi(...);\n\n// now we load our page\nconst page = await sp.web.loadClientsidePage(\"/sites/dev/SitePages/List-Web-Part.aspx\");\n\n// get our part and pass it to the constructor of our wrapper class\nconst part = new ListWebpart(page.sections[0].columns[0].getControl(0));\n\npart.DisplayTitle = \"My New Title!\";\n\nawait page.save();\n

Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.

"},{"location":"sp/clientside-pages/#page-operations","title":"Page Operations","text":"

There are other operation you can perform on a page in addition to manipulating the content.

"},{"location":"sp/clientside-pages/#pagelayout","title":"pageLayout","text":"

You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.pageLayout;\n\n// set the value\npage.pageLayout = \"Article\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#bannerimageurl","title":"bannerImageUrl","text":"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.bannerImageUrl;\n\n// set the value\npage.bannerImageUrl = \"/server/relative/path/to/image.png\";\nawait page.save();\n

Banner images need to exist within the same site collection as the page where you want to use them.

"},{"location":"sp/clientside-pages/#thumbnailurl","title":"thumbnailUrl","text":"

Allows you to set the thumbnail used for the page independently of the banner.

If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.thumbnailUrl;\n\n// set the value\npage.thumbnailUrl = \"/server/relative/path/to/image.png\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#topicheader","title":"topicHeader","text":"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.topicHeader;\n\n// set the value\npage.topicHeader = \"My cool header!\";\nawait page.save();\n\n// clear the topic header and hide it\npage.topicHeader = \"\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#title","title":"title","text":"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.title;\n\n// set the value\npage.title = \"My page title\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#description","title":"description","text":"

Descriptions are limited to 255 chars

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.description;\n\n// set the value\npage.description = \"A description\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#layouttype","title":"layoutType","text":"

Sets the layout type of the page. The valid values are: \"FullWidthImage\", \"NoImage\", \"ColorBlock\", \"CutInShape\"

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.layoutType;\n\n// set the value\npage.layoutType = \"ColorBlock\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#headertextalignment","title":"headerTextAlignment","text":"

Sets the header text alignment to one of \"Left\" or \"Center\"

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.headerTextAlignment;\n\n// set the value\npage.headerTextAlignment = \"Center\";\nawait page.save();\n
"},{"location":"sp/clientside-pages/#showtopicheader","title":"showTopicHeader","text":"

Sets if the topic header is displayed on a page.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.showTopicHeader;\n\n// show the header\npage.showTopicHeader = true;\nawait page.save();\n\n// hide the header\npage.showTopicHeader = false;\nawait page.save();\n
"},{"location":"sp/clientside-pages/#showpublishdate","title":"showPublishDate","text":"

Sets if the publish date is displayed on a page.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the current value\nconst value = page.showPublishDate;\n\n// show the date\npage.showPublishDate = true;\nawait page.save();\n\n// hide the date\npage.showPublishDate = false;\nawait page.save();\n
"},{"location":"sp/clientside-pages/#get-set-author-details","title":"Get / Set author details","text":"
import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/site-users\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// get the author details (string | null)\nconst value = page.authorByLine;\n\n// set the author by user id\nconst user = await sp.web.currentUser.select(\"Id\", \"LoginName\")();\nconst userId = user.Id;\nconst userLogin = user.LoginName;\n\nawait page.setAuthorById(userId);\nawait page.save();\n\nawait page.setAuthorByLoginName(userLogin);\nawait page.save();\n

you must still save the page after setting the author to persist your changes as shown in the example.

"},{"location":"sp/clientside-pages/#load","title":"load","text":"

Loads the page from the server. This will overwrite any local unsaved changes.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nawait page.load();\n
"},{"location":"sp/clientside-pages/#save","title":"save","text":"

Uncustomized home pages (i.e the home page that is generated with a site out of the box) cannot be updated by this library without becoming corrupted.

Saves any changes to the page, optionally keeping them in draft state.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// changes are published\nawait page.save();\n\n// changes remain in draft\nawait page.save(false);\n
"},{"location":"sp/clientside-pages/#discardpagecheckout","title":"discardPageCheckout","text":"

Discards any current checkout of the page by the current user.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nawait page.discardPageCheckout();\n
"},{"location":"sp/clientside-pages/#schedulepublish","title":"schedulePublish","text":"

Schedules the page for publishing.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// date and time to publish the page in UTC.\nconst publishDate = new Date(\"1/1/1901\");\n\nconst scheduleVersion: string = await page.schedulePublish(publishDate);\n
"},{"location":"sp/clientside-pages/#promotetonews","title":"promoteToNews","text":"

Promotes the page as a news article.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nawait page.promoteToNews();\n
"},{"location":"sp/clientside-pages/#enablecomments-disablecomments","title":"enableComments & disableComments","text":"

Used to control the availability of comments on a page.

import { spfi } from \"@pnp/sp\";\n// you need to import the comments sub-module or use the all preset\nimport \"@pnp/sp/comments/clientside-page\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// turn on comments\nawait page.enableComments();\n\n// turn off comments\nawait page.disableComments();\n
"},{"location":"sp/clientside-pages/#findcontrolbyid","title":"findControlById","text":"

Finds a control within the page by id.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage, ClientsideText } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\nconst control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\");\n\n// you can also type the control\nconst control = page.findControlById<ClientsideText>(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\");\n
"},{"location":"sp/clientside-pages/#findcontrol","title":"findControl","text":"

Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// find the first control whose order is 9\nconst control = page.findControl((c) => c.order === 9);\n\n// iterate all the controls and output the id to the console\npage.findControl((c) => {\n    console.log(c.id);\n    return false;\n});\n
"},{"location":"sp/clientside-pages/#like-unlike","title":"like & unlike","text":"

Updates the page's like value for the current user.

// our page instance\nconst page: IClientsidePage;\n\n// like this page\nawait page.like();\n\n// unlike this page\nawait page.unlike();\n
"},{"location":"sp/clientside-pages/#getlikedbyinformation","title":"getLikedByInformation","text":"

Gets the likes information for this page.

// our page instance\nconst page: IClientsidePage;\n\nconst info = await page.getLikedByInformation();\n
"},{"location":"sp/clientside-pages/#copy","title":"copy","text":"

Creates a copy of the page, including all controls.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// creates a published copy of the page\nconst pageCopy = await page.copy(sp.web, \"newpagename\", \"New Page Title\");\n\n// creates a draft (unpublished) copy of the page\nconst pageCopy2 = await page.copy(sp.web, \"newpagename\", \"New Page Title\", false);\n\n// edits to pageCopy2 ...\n\n// publish the page\npageCopy2.save();\n
"},{"location":"sp/clientside-pages/#copyto","title":"copyTo","text":"

Copies the contents of a page to another existing page instance.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// our page instances, loaded in any of the ways shown above\nconst source: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\nconst target: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/target.aspx\");\nconst target2: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/target2.aspx\");\n\n// creates a published copy of the page\nawait source.copyTo(target);\n\n// creates a draft (unpublished) copy of the page\nawait source.copyTo(target2, false);\n\n// edits to target2...\n\n// publish the page\ntarget2.save();\n
"},{"location":"sp/clientside-pages/#setbannerimage","title":"setBannerImage","text":"

Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent.

Banner images need to exist within the same site collection as the page where you want to use them.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\npage.setBannerImage(\"/server/relative/path/to/image.png\");\n\n// save the changes\nawait page.save();\n\n// set additional props\npage.setBannerImage(\"/server/relative/path/to/image.png\", {\n  altText: \"Image description\",\n  imageSourceType: 2,\n  translateX: 30,\n  translateY: 1234,\n});\n\n// save the changes\nawait page.save();\n

This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar.

import { join } from \"path\";\nimport { createReadStream } from \"fs\";\nimport { spfi, SPFI, SPFx } from \"@pnp/sp\";\nimport { SPDefault } from \"@pnp/nodejs\";\nimport { LogLevel  } from \"@pnp/logging\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/clientside-pages\";\n\nconst buffer = readFileSync(\"c:/temp/key.pem\");\n\nconst config:any = {\n  auth: {\n    authority: \"https://login.microsoftonline.com/{my tenant}/\",\n    clientId: \"{application (client) id}\",\n    clientCertificate: {\n      thumbprint: \"{certificate thumbprint, displayed in AAD}\",\n      privateKey: buffer.toString(),\n    },\n  },\n  system: {\n    loggerOptions: {\n      loggerCallback(loglevel: any, message: any, containsPii: any) {\n          console.log(message);\n      },\n      piiLoggingEnabled: false,\n      logLevel: LogLevel.Verbose\n    }\n  }\n};\n\n// configure your node options\nconst sp = spfi('{site url}').using(SPDefault({\n  baseUrl: '{site url}',\n  msal: {\n    config: config,\n    scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]\n  }\n}));\n\n\n// add the banner image\nconst dirname = join(\"C:/path/to/file\", \"img-file.jpg\");\n\nconst chunkedFile = createReadStream(dirname);\n\nconst far = await sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents\").files.addChunked( \"banner.jpg\", chunkedFile );\n\n// add the page\nconst page = await sp.web.addClientsidePage(\"MyPage\", \"Page Title\");\n\n// set the banner image\npage.setBannerImage(far.data.ServerRelativeUrl);\n\n// publish the page\nawait page.save();\n
"},{"location":"sp/clientside-pages/#setbannerimagefromexternalurl","title":"setBannerImageFromExternalUrl","text":"

Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there.

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// you must await this method\nawait page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\");\n\n// save the changes\nawait page.save();\n

You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// you must await this method\nawait page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\", {\n  altText: \"Image description\",\n  imageSourceType: 2,\n  translateX: 30,\n  translateY: 1234,\n});\n\n// save the changes\nawait page.save();\n
"},{"location":"sp/clientside-pages/#recycle","title":"recycle","text":"

Allows you to recycle a page without first needing to use getItem

// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.recycle();\n
"},{"location":"sp/clientside-pages/#delete","title":"delete","text":"

Allows you to delete a page without first needing to use getItem

// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.delete();\n
"},{"location":"sp/clientside-pages/#saveastemplate","title":"saveAsTemplate","text":"

Save page as a template from which other pages can be created. If it doesn't exist a special folder \"Templates\" will be added to the doc lib

// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.saveAsTemplate();\n// save a template, but don't publish it allowing you to make changes before it is available to users\n// you \nawait page.saveAsTemplate(false);\n// ... changes to the page\n// you must publish the template so it is available\nawait page.save();\n
"},{"location":"sp/clientside-pages/#share","title":"share","text":"

Allows sharing a page with one or more email addresses, optionall including a message in the email

// our page instance\nconst page: IClientsidePage;\n// you must await this method\nawait page.share([\"email@place.com\", \"email2@otherplace.com\"]);\n// optionally include a message\nawait page.share([\"email@place.com\", \"email2@otherplace.com\"], \"Please check out this cool page!\");\n
"},{"location":"sp/clientside-pages/#add-repost-page","title":"Add Repost Page","text":"

You can use the addRepostPage method to add a report page. The method returns the absolute url of the created page. All properties are optional but it is recommended to include as much as possible to improve the quality of the repost card's display.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages\";\n\nconst sp = spfi(...);\nconst page = await sp.web.addRepostPage({\n    BannerImageUrl: \"https://some.absolute/path/to/an/image.jpg\",\n    IsBannerImageUrlExternal: true,\n    Description: \"My Description\",\n    Title: \"This is my title!\",\n    OriginalSourceUrl: \"https://absolute/path/to/article\",\n});\n

To specify an existing item in another list all of the four properties OriginalSourceSiteId, OriginalSourceWebId, OriginalSourceListId, and OriginalSourceItemId are required.

"},{"location":"sp/column-defaults/","title":"@pnp/sp/column-defaults","text":"

The column defaults sub-module allows you to manage the default column values on a library or library folder.

"},{"location":"sp/column-defaults/#get-folder-defaults","title":"Get Folder Defaults","text":"

You can get the default values for a specific folder as shown below:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nconst defaults = await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").getDefaultColumnValues();\n\n/*\nThe resulting structure will have the form:\n\n[\n  {\n    \"name\": \"{field internal name}\",\n    \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\",\n    \"value\": \"{the default value}\"\n  },\n  {\n    \"name\": \"{field internal name}\",\n    \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\",\n    \"value\": \"{the default value}\"\n  }\n]\n*/\n
"},{"location":"sp/column-defaults/#set-folder-defaults","title":"Set Folder Defaults","text":"

When setting the defaults for a folder you need to include the field's internal name and the value.

For more examples of other field types see the section Pattern for setting defaults on various column types

Note: Be very careful when setting the path as the site collection url is case sensitive

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").setDefaultColumnValues([{\n  name: \"TextField\",\n  value: \"Something\",\n},\n{\n  name: \"NumberField\",\n  value: 14,\n}]);\n
"},{"location":"sp/column-defaults/#get-library-defaults","title":"Get Library Defaults","text":"

You can also get all of the defaults for the entire library.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nconst defaults = await sp.web.lists.getByTitle(\"DefaultColumnValues\").getDefaultColumnValues();\n\n/*\nThe resulting structure will have the form:\n\n[\n  {\n    \"name\": \"{field internal name}\",\n    \"path\": \"/sites/dev/DefaultColumnValues\",\n    \"value\": \"{the default value}\"\n  },\n  {\n    \"name\": \"{field internal name}\",\n    \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\",\n    \"value\": \"{a different default value}\"\n  }\n]\n*/\n
"},{"location":"sp/column-defaults/#set-library-defaults","title":"Set Library Defaults","text":"

You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value.

For more examples of other field types see the section Pattern for setting defaults on various column types

Note: Be very careful when setting the path as the site collection url is case sensitive

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([{\n  name: \"TextField\",\n  path: \"/sites/dev/DefaultColumnValues\",\n  value: \"#PnPjs Rocks!\",\n}]);\n
"},{"location":"sp/column-defaults/#clear-folder-defaults","title":"Clear Folder Defaults","text":"

If you want to clear all of the folder defaults you can use the clear method:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").clearDefaultColumnValues();\n
"},{"location":"sp/column-defaults/#clear-library-defaults","title":"Clear Library Defaults","text":"

If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/column-defaults\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([]);\n
"},{"location":"sp/column-defaults/#pattern-for-setting-defaults-on-various-column-types","title":"Pattern for setting defaults on various column types","text":"

The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types.

[{\n    // Text/Boolean/CurrencyDateTime/Choice/User\n    name: \"TextField\":\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: \"#PnPjs Rocks!\",\n}, {\n    //Number\n    name: \"NumberField\",\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: 42,\n}, {\n    //Date\n    name: \"NumberField\",\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: \"1900-01-01T00:00:00Z\",\n}, {\n    //Date - Today\n    name: \"NumberField\",\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: \"[today]\",\n}, {\n    //MultiChoice\n    name: \"MultiChoiceField\",\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: [\"Item 1\", \"Item 2\"],\n}, {\n    //MultiChoice - single value\n    name: \"MultiChoiceField\",\n    path: \"/sites/dev/DefaultColumnValues/folder2\",\n    value: [\"Item 1\"],\n}, {\n    //Taxonomy - single value\n    name: \"TaxonomyField\",\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: {\n        wssId:\"-1\",\n        termName: \"TaxValueName\",\n        termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\"\n        }\n}, {\n    //Taxonomy - multiple value\n    name: \"TaxonomyMultiField\",\n    path: \"/sites/dev/DefaultColumnValues\",\n    value: [{\n        wssId:\"-1\",\n        termName: \"TaxValueName\",\n        termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\"\n        },{\n        wssId:\"-1\",\n        termName: \"TaxValueName2\",\n        termId: \"95d4c307-dde5-49d8-b861-392e145d94d3\"\n        },]\n}]);\n
"},{"location":"sp/column-defaults/#taxonomy-full-example","title":"Taxonomy Full Example","text":"

This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/column-defaults\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get the term's info we want to use as the default\nconst term = await sp.termStore.sets.getById(\"ea6fc521-d293-4f3d-9e84-f3a5bc0936ce\").getTermById(\"775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a\")();\n\n// get the default term label\nconst defLabel = term.labels.find(v => v.isDefault);\n\n// set the default value using -1, the term id, and the term's default label name\nawait sp.web.lists.getByTitle(\"MetaDataDocLib\").rootFolder.setDefaultColumnValues([{\n  name: \"MetaDataColumnInternalName\",\n  value: {\n      wssId: \"-1\",\n      termId: term.id,\n      termName: defLabel.name,\n  }\n}])\n\n// check that the defaults have updated\nconst newDefaults = await sp.web.lists.getByTitle(\"MetaDataDocLib\").getDefaultColumnValues();\n
"},{"location":"sp/comments-likes/","title":"@pnp/sp/comments and likes","text":"

Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles.

These APIs are currently in BETA and are subject to change or may not work on all tenants.

"},{"location":"sp/comments-likes/#clientsidepage-comments","title":"ClientsidePage Comments","text":"

The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.

"},{"location":"sp/comments-likes/#add-comments","title":"Add Comments","text":"

You can add a comment using the addComment method as shown

import { spfi } from \"@pnp/sp\";\nimport { CreateClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\");\n// optionally publish the page first\nawait page.save();\n\n//add a comment as text\nconst comment = await page.addComment(\"A test comment\");\n\n//or you can include the @mentions. html anchor required to include mention in text body.\nconst mentionHtml = `<a data-sp-mention-user-id=\"test@contoso.com\" href=\"mailto&#58;test@contoso.com.com\" tabindex=\"-1\">Test User</a>`;\n\nconst commentInfo: Partial<ICommentInfo> = { text: `${mentionHtml} This is the test comment with at mentions`, \n    mentions: [{ loginName: 'test@contoso.com', email: 'test@contoso.com', name: 'Test User' }], };\nconst comment = await page.addComment(commentInfo);\n
"},{"location":"sp/comments-likes/#get-page-comments","title":"Get Page Comments","text":"
import { spfi } from \"@pnp/sp\";\nimport { CreateClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\");\n// optionally publish the page first\nawait page.save();\n\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\nawait page.addComment(\"A test comment\");\n\nconst comments = await page.getComments();\n
"},{"location":"sp/comments-likes/#enablecomments-disablecomments","title":"enableComments & disableComments","text":"

Used to control the availability of comments on a page

import { spfi } from \"@pnp/sp\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n// you need to import the comments sub-module or use the all preset\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// our page instance\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// turn on comments\nawait page.enableComments();\n\n// turn off comments\nawait page.disableComments();\n
"},{"location":"sp/comments-likes/#getbyid","title":"GetById","text":"
import { spfi } from \"@pnp/sp\";\nimport { CreateClientsidePage } from \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\");\n// optionally publish the page first\nawait page.save();\n\nconst comment = await page.addComment(\"A test comment\");\n\nconst commentData = await page.getCommentById(parseInt(comment.id, 10));\n
"},{"location":"sp/comments-likes/#clear-comments","title":"Clear Comments","text":""},{"location":"sp/comments-likes/#item-comments","title":"Item Comments","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/comments/item\";\n\nconst sp = spfi(...);\n\nconst item = await sp.web.getFileByServerRelativePath(\"/sites/dev/SitePages/Test_8q5L.aspx\").getItem();\n\n// as an example, or any of the below options\nawait item.like();\n

The below examples use a variable named \"item\" which is taken to represent an IItem instance.

"},{"location":"sp/comments-likes/#comments","title":"Comments","text":""},{"location":"sp/comments-likes/#get-item-comments","title":"Get Item Comments","text":"
const comments = await item.comments();\n

You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:

import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\n// these will be Comment instances in the array\ncomments[0].replies.add({ text: \"#PnPjs is pretty ok!\" });\n\n//load the top 20 replies and comments for an item including likedBy information\nconst comments = await item.comments.expand(\"replies\", \"likedBy\", \"replies/likedBy\").top(20)();\n
"},{"location":"sp/comments-likes/#add-comment","title":"Add Comment","text":"
import { spfi } from \"@pnp/sp\";\nimport { ICommentInfo } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\n// you can add a comment as a string\nconst comment = await item.comments.add(\"string comment\");\n\n\n
"},{"location":"sp/comments-likes/#delete-a-comment","title":"Delete a Comment","text":"
import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\n// these will be Comment instances in the array\ncomments[0].delete()\n
"},{"location":"sp/comments-likes/#like-comment","title":"Like Comment","text":"
import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\n// these will be Comment instances in the array\ncomments[0].like();\n
"},{"location":"sp/comments-likes/#unlike-comment","title":"Unlike Comment","text":"
import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\ncomments[0].unlike()\n
"},{"location":"sp/comments-likes/#reply-to-a-comment","title":"Reply to a Comment","text":"
import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\nconst comment = await comments[0].comments.add({ text: \"#PnPjs is pretty ok!\" });\n
"},{"location":"sp/comments-likes/#load-replies-to-a-comment","title":"Load Replies to a Comment","text":"
import { spfi } from \"@pnp/sp\";\nimport { IComments } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst comments: IComments = await item.comments();\n\nconst replies = await comments[0].replies();\n
"},{"location":"sp/comments-likes/#likeunlike","title":"Like/Unlike","text":"

You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/comments\";\nimport { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst item = sp.web.lists.getByTitle(\"PnP List\").items.getById(1);\n\n// like an item\nawait item.like();\n\n// unlike an item\nawait item.unlike();\n\n// get the liked by information\nconst likedByInfo: ILikedByInformation = await item.getLikedByInformation();\n

To like/unlike a client-side page and get liked by information.

import { spfi } from \"@pnp/sp\";\nimport { ILikedByInformation } from \"@pnp/sp/comments\";\nimport { IClientsidePage } from \"@pnp/sp/clientside-pages\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages\";\nimport \"@pnp/sp/comments/clientside-page\";\n\nconst sp = spfi(...);\n\nconst page: IClientsidePage = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/home.aspx\");\n\n// like a page\nawait page.like();\n\n// unlike a page\nawait page.unlike();\n\n// get the liked by information\nconst likedByInfo: ILikedByInformation = await page.getLikedByInformation();\n
"},{"location":"sp/comments-likes/#rate","title":"Rate","text":"

You can rate list items with a numeric values between 1 and 5.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/comments\";\nimport { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\";\n\nconst sp = spfi(...);\n\nconst item = sp.web.lists.getByTitle(\"PnP List\").items.getById(1);\n\n// rate an item\nawait item.rate(2);\n
"},{"location":"sp/content-types/","title":"@pnp/sp/content-types","text":"

Content Types are used to define sets of columns in SharePoint.

"},{"location":"sp/content-types/#icontenttypes","title":"IContentTypes","text":""},{"location":"sp/content-types/#add-an-existing-content-type-to-a-collection","title":"Add an existing Content Type to a collection","text":"

The following example shows how to add the built in Picture Content Type to the Documents library.

const sp = spfi(...);\n\nsp.web.lists.getByTitle(\"Documents\").contentTypes.addAvailableContentType(\"0x010102\");\n
"},{"location":"sp/content-types/#get-a-content-type-by-id","title":"Get a Content Type by Id","text":"
import { IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\nconst d: IContentType = await sp.web.contentTypes.getById(\"0x01\")();\n\n// log content type name to console\nconsole.log(d.name);\n
"},{"location":"sp/content-types/#update-a-content-type","title":"Update a Content Type","text":"
import { IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\nawait sp.web.contentTypes.getById(\"0x01\").update({EditFormClientSideComponentId: \"9dfdb916-7380-4b69-8d92-bc711f5fa339\"});\n
"},{"location":"sp/content-types/#add-a-new-content-type","title":"Add a new Content Type","text":"

To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.

const sp = spfi(...);\n\nsp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\");\n

It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API).

const sp = spfi(...);\n\n//Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings)\nsp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\", \"This is my content type.\", \"_PnP Content Types\", { ReadOnly: true });\n
"},{"location":"sp/content-types/#icontenttype","title":"IContentType","text":""},{"location":"sp/content-types/#get-the-field-links","title":"Get the field links","text":"

Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get field links from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").fieldLinks();\n\n// log collection of fieldlinks to console\nconsole.log(d);\n
"},{"location":"sp/content-types/#get-content-type-fields","title":"Get Content Type fields","text":"

To get a collection with all fields on the Content Type, simply use this method.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get fields from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").fields();\n\n// log collection of fields to console\nconsole.log(d);\n
"},{"location":"sp/content-types/#get-parent-content-type","title":"Get parent Content Type","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get parent Content Type from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").parent();\n\n// log name of parent Content Type to console\nconsole.log(d.Name)\n
"},{"location":"sp/content-types/#get-content-type-workflow-associations","title":"Get Content Type Workflow associations","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { ContentType, IContentType } from \"@pnp/sp/content-types\";\n\nconst sp = spfi(...);\n\n// get workflow associations from built in Content Type Document (Id: \"0x0101\")\nconst d = await sp.web.contentTypes.getById(\"0x0101\").workflowAssociations();\n\n// log collection of workflow associations to console\nconsole.log(d);\n
"},{"location":"sp/context-info/","title":"@pnp/sp/ - context-info","text":"

Starting with 3.8.0 we've moved context information to its own sub-module. You can now import context-info and use it on any SPQueryable derived object to understand the context. Some examples are below.

"},{"location":"sp/context-info/#icontextinfo","title":"IContextInfo","text":"

The information returned by the method is defined by the IContextInfo interface.

export interface IContextInfo {\n    FormDigestTimeoutSeconds: number;\n    FormDigestValue: number;\n    LibraryVersion: string;\n    SiteFullUrl: string;\n    SupportedSchemaVersions: string[];\n    WebFullUrl: string;\n}\n
"},{"location":"sp/context-info/#get-context-for-a-web","title":"Get Context for a web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/context-info\";\n\nconst sp = spfi(...);\n\nconst info = await sp.web.getContextInfo();\n
"},{"location":"sp/context-info/#get-context-from-lists","title":"Get Context from lists","text":"

This pattern works as well for any SPQueryable derived object, allowing you to gain context no matter with which fluent objects you are working.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/context-info\";\n\nconst sp = spfi(...);\n\nconst info = await sp.web.lists.getContextInfo();\n
"},{"location":"sp/context-info/#get-context-from-url","title":"Get Context from URL","text":"

Often you will have an absolute URL to a file or path and would like to create an IWeb or IFile. You can use the fileFromPath or folderFromPath to get an IFile/IFolder, or you can use getContextInfo to create a new web within the context of the file path.

import { spfi } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/context-info\";\n\nconst sp = spfi(...);\n\n// supply an absolute path to get associated context info, this works across site collections\nconst { WebFullUrl } = await sp.web.getContextInfo(\"https://tenant.sharepoint.com/sites/dev/shared documents/file.docx\");\n\n// create a new web pointing to the web where the file is stored\nconst web = Web([sp.web, decodeURI(WebFullUrl)]);\n\nconst webInfo = await web();\n
"},{"location":"sp/favorites/","title":"@pnp/sp/ - favorites","text":"

The favorites API allows you to fetch and manipulate followed sites and list items (also called saved for later). Note, all of these methods only work with the context of a logged in user, and not with app-only permissions.

"},{"location":"sp/favorites/#get-current-users-followed-sites","title":"Get current user's followed sites","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst favSites = await sp.favorites.getFollowedSites();\n
"},{"location":"sp/favorites/#add-a-site-to-current-users-followed-sites","title":"Add a site to current user's followed sites","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst tenantUrl = \"contoso.sharepoint.com\";\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\"\nconst webUrl = \"https://contoso.sharepoint.com/sites/favsite\"\n\nconst favSiteInfo = await sp.favorites.getFollowedSites.add(tenantUrl, siteId, webId, webUrl);\n
"},{"location":"sp/favorites/#remove-a-site-from-current-users-followed-sites","title":"Remove a site from current user's followed sites","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst tenantUrl = \"contoso.sharepoint.com\";\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\"\nconst webUrl = \"https://contoso.sharepoint.com/sites/favsite\"\n\nawait sp.favorites.getFollowedSites.remove(tenantUrl, siteId, webId, webUrl);\n
"},{"location":"sp/favorites/#get-current-users-followed-list-items","title":"Get current user's followed list items","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst favListItems = await sp.favorites.getFollowedListItems();\n
"},{"location":"sp/favorites/#add-an-item-to-current-users-followed-list-items","title":"Add an item to current user's followed list items","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\";\nconst listId = \"f09fe67e-0160-4fcc-9144-905bd4889f31\";\nconst listItemUniqueId = \"1425C841-626A-44C9-8731-DA8BDC0882D1\";\n\nconst favListItemInfo = await sp.favorites.getFollowedListItems.add(siteId, webId, listId, listItemUniqueId);\n
"},{"location":"sp/favorites/#remove-an-item-from-current-users-followed-list-items","title":"Remove an item from current user's followed list items","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/favorites\";\n\nconst sp = spfi(...);\n\nconst siteId = \"e3913de9-bfee-4089-b1bc-fb147d302f11\";\nconst webId = \"11a53c2b-0a67-46c8-8599-db50b8bc4dd1\";\nconst listId = \"f09fe67e-0160-4fcc-9144-905bd4889f31\";\nconst listItemUniqueId = \"1425C841-626A-44C9-8731-DA8BDC0882D1\";\n\nconst favListItemInfo = await sp.favorites.getFollowedListItems.remove(siteId, webId, listId, listItemUniqueId);\n
"},{"location":"sp/features/","title":"@pnp/sp/features","text":"

Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.

"},{"location":"sp/features/#ifeatures","title":"IFeatures","text":"

Represents a collection of features. SharePoint Sites and Webs will have a collection of features

"},{"location":"sp/features/#getbyid","title":"getById","text":"

Gets the information about a feature for the given GUID

import { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nconst webFeature = await sp.web.features.getById(webFeatureId)();\n\nconst siteFeatureId = \"guid-of-site-scope-feature\";\nconst siteFeature = await sp.site.features.getById(siteFeatureId)();\n
"},{"location":"sp/features/#add","title":"add","text":"

Adds (activates) a feature at the Site or Web level

import { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nlet res = await sp.web.features.add(webFeatureId);\n// Activate with force\nres = await sp.web.features.add(webFeatureId, true);\n
"},{"location":"sp/features/#remove","title":"remove","text":"

Removes and deactivates the specified feature from the SharePoint Site or Web

import { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nlet res = await sp.web.features.remove(webFeatureId);\n// Deactivate with force\nres = await sp.web.features.remove(webFeatureId, true);\n
"},{"location":"sp/features/#ifeature","title":"IFeature","text":"

Represents an instance of a SharePoint feature.

"},{"location":"sp/features/#deactivate","title":"deactivate","text":"

Deactivates the specified feature from the SharePoint Site or Web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/features\";\n\nconst sp = spfi(...);\n\n//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a\nconst webFeatureId = \"guid-of-web-feature\";\nsp.web.features.remove(webFeatureId);\n\n// Deactivate with force\nsp.web.features.remove(webFeatureId, true);\n
"},{"location":"sp/fields/","title":"@pnp/sp/fields","text":"

Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.

"},{"location":"sp/fields/#ifields","title":"IFields","text":""},{"location":"sp/fields/#get-field-by-id","title":"Get Field by Id","text":"

Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldInfo } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/fields\";\n\n// set up sp root object\nconst sp = spfi(...);\n// get the field by Id for web\nconst field: IField = sp.web.fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\");\n// get the field by Id for list 'My List'\nconst field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\")();\n\n// we can use this 'field' variable to execute more queries on the field:\nconst r = await field.select(\"Title\")();\n\n// show the response from the server\nconsole.log(r.Title);\n
"},{"location":"sp/fields/#get-field-by-title","title":"Get Field by Title","text":"

You can also get a field from the collection by title.

import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldInfo } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\"\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n// get the field with the title 'Author' for web\nconst field: IField = sp.web.fields.getByTitle(\"Author\");\n// get the field with the title 'Title' for list 'My List'\nconst field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Title\")();\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#get-field-by-internal-name-or-title","title":"Get Field by Internal Name or Title","text":"

You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different.

import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldInfo } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\"\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n// get the field with the internal name 'ModifiedBy' for web\nconst field: IField = sp.web.fields.getByInternalNameOrTitle(\"ModifiedBy\");\n// get the field with the internal name 'ModifiedBy' for list 'My List'\nconst field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByInternalNameOrTitle(\"ModifiedBy\")();\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#create-a-field-using-an-xml-schema","title":"Create a Field using an XML schema","text":"

Create a new field by defining an XML schema that assigns all the properties for the field.

import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldAddResult } from \"@pnp/sp/fields/types\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// define the schema for your new field, in this case a date field with a default date of today.\nconst fieldSchema = `<Field ID=\"{03b09ff4-d99d-45ed-841d-3855f77a2483}\" StaticName=\"MyField\" Name=\"MyField\" DisplayName=\"My New Field\" FriendlyDisplayFormat=\"Disabled\" Format=\"DateOnly\" Type=\"DateTime\" Group=\"My Group\"><Default>[today]</Default></Field>`;\n\n// create the new field in the web\nconst field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema);\n// create the new field in the list 'My List'\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(fieldSchema);\n\n// we can use this 'field' variable to run more queries on the list:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-new-field","title":"Add a New Field","text":"

Use the add method to create a new field where you define the field type

import { spfi } from \"@pnp/sp\";\nimport { IField, IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.add(\"My Field\", FieldTypes.Text, { FieldTypeKind: 3, Group: \"My Group\" });\n// create a new field called 'My Field' in the list 'My List'\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.add(\"My Field\", FieldTypes.Text, { FieldTypeKind: 3, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-site-field-to-a-list","title":"Add a Site Field to a List","text":"

Use the createFieldAsXml method to add a site field to a list.

import { spfi } from \"@pnp/sp\";\nimport { IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.add(\"My Field\", FieldTypes.Text, { FieldTypeKind: 3, Group: \"My Group\" });\n// add the site field 'My Field' to the list 'My List'\nconst r = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(field.data.SchemaXml as string);\n\n// log the field Id to console\nconsole.log(r.data.Id);\n
"},{"location":"sp/fields/#add-a-text-field","title":"Add a Text Field","text":"

Use the addText method to create a new text field.

import { spfi } from \"@pnp/sp\";\nimport { IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new text field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.addText(\"My Field\", { MaxLength: 255, Group: \"My Group\" });\n// create a new text field called 'My Field' in the list 'My List'.\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addText(\"My Field\", { MaxLength: 255, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-calculated-field","title":"Add a Calculated Field","text":"

Use the addCalculated method to create a new calculated field.

import { spfi } from \"@pnp/sp\";\nimport { DateTimeFieldFormatType, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new calculated field called 'My Field' in web\nconst field = await sp.web.fields.addCalculated(\"My Field\", { Formula: \"=Modified+1\", DateFormat: DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: \"MyGroup\" });\n// create a new calculated field called 'My Field' in the list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCalculated(\"My Field\", { Formula: \"=Modified+1\", DateFormat:  DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: \"MyGroup\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-datetime-field","title":"Add a Date/Time Field","text":"

Use the addDateTime method to create a new date/time field.

import { spfi } from \"@pnp/sp\";\nimport { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new date/time field called 'My Field' in web\nconst field = await sp.web.fields.addDateTime(\"My Field\", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled,  Group: \"My Group\" });\n// create a new date/time field called 'My Field' in the list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addDateTime(\"My Field\", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-currency-field","title":"Add a Currency Field","text":"

Use the addCurrency method to create a new currency field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new currency field called 'My Field' in web\nconst field = await sp.web.fields.addCurrency(\"My Field\", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: \"My Group\" });\n// create a new currency field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCurrency(\"My Field\", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-an-image-field","title":"Add an Image Field","text":"

Use the addImageField method to create a new image field.

import { spfi } from \"@pnp/sp\";\nimport { IFieldAddResult, FieldTypes } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new image field called 'My Field' in web.\nconst field: IFieldAddResult = await sp.web.fields.addImageField(\"My Field\");\n// create a new image field called 'My Field' in the list 'My List'.\nconst field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addImageField(\"My Field\");\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-multi-line-text-field","title":"Add a Multi-line Text Field","text":"

Use the addMultilineText method to create a new multi-line text field.

For Enhanced Rich Text mode, see the next section.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new multi-line text field called 'My Field' in web\nconst field = await sp.web.fields.addMultilineText(\"My Field\", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: \"My Group\" });\n// create a new multi-line text field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultilineText(\"My Field\", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-multi-line-text-field-with-enhanced-rich-text","title":"Add a Multi-line Text Field with Enhanced Rich Text","text":"

The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n//Create a new multi-line text field called 'My Field' in web\nconst field = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(\n    `<Field Type=\"Note\" Name=\"MyField\" DisplayName=\"My Field\" Required=\"FALSE\" RichText=\"TRUE\" RichTextMode=\"FullHtml\" />`\n);\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-number-field","title":"Add a Number Field","text":"

Use the addNumber method to create a new number field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new number field called 'My Field' in web\nconst field = await sp.web.fields.addNumber(\"My Field\", { MinimumValue: 1, MaximumValue: 100, Group: \"My Group\" });\n// create a new number field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addNumber(\"My Field\", { MinimumValue: 1, MaximumValue: 100, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-url-field","title":"Add a URL Field","text":"

Use the addUrl method to create a new url field.

import { spfi } from \"@pnp/sp\";\nimport { UrlFieldFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new url field called 'My Field' in web\nconst field = await sp.web.fields.addUrl(\"My Field\", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: \"My Group\" });\n// create a new url field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUrl(\"My Field\", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-user-field","title":"Add a User Field","text":"

Use the addUser method to create a new user field.

import { spfi } from \"@pnp/sp\";\nimport { FieldUserSelectionMode } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new user field called 'My Field' in web\nconst field = await sp.web.fields.addUser(\"My Field\", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: \"My Group\" });\n// create a new user field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUser(\"My Field\", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n\n// **\n// Adding a lookup that supports multiple values takes two calls:\nconst fieldAddResult = await sp.web.fields.addUser(\"Multi User Field\", { SelectionMode: FieldUserSelectionMode.PeopleOnly });\nawait fieldAddResult.field.update({ AllowMultipleValues: true }, \"SP.FieldUser\");\n
"},{"location":"sp/fields/#add-a-lookup-field","title":"Add a Lookup Field","text":"

Use the addLookup method to create a new lookup field.

import { spfi } from \"@pnp/sp\";\nimport { FieldTypes } from \"@pnp/sp/fields/types\";\n\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst list = await sp.web.lists.getByTitle(\"My Lookup List\")();\n// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web.\nconst field = await sp.web.fields.addLookup(\"My Field\", { LookupListId: list.data.Id, LookupFieldName: \"Title\" });\n// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", {LookupListId: list.data.Id, LookupFieldName: \"Title\"});\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n\n// **\n// Adding a lookup that supports multiple values takes two calls:\nconst fieldAddResult = await sp.web.fields.addLookup(\"Multi Lookup Field\", { LookupListId: list.data.Id, LookupFieldName: \"Title\" });\nawait fieldAddResult.field.update({ AllowMultipleValues: true }, \"SP.FieldLookup\");\n
"},{"location":"sp/fields/#add-a-choice-field","title":"Add a Choice Field","text":"

Use the addChoice method to create a new choice field.

import { spfi } from \"@pnp/sp\";\nimport { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];\n// create a new choice field called 'My Field' in web\nconst field = await sp.web.fields.addChoice(\"My Field\", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: \"My Group\" });\n// create a new choice field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addChoice(\"My Field\", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-multi-choice-field","title":"Add a Multi-Choice Field","text":"

Use the addMultiChoice method to create a new multi-choice field.

import { spfi } from \"@pnp/sp\";\nimport { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];\n// create a new multi-choice field called 'My Field' in web\nconst field = await sp.web.fields.addMultiChoice(\"My Field\", { Choices: choices, FillInChoice: false, Group: \"My Group\" });\n// create a new multi-choice field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultiChoice(\"My Field\", { Choices: choices, FillInChoice: false, Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-boolean-field","title":"Add a Boolean Field","text":"

Use the addBoolean method to create a new boolean field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new boolean field called 'My Field' in web\nconst field = await sp.web.fields.addBoolean(\"My Field\", { Group: \"My Group\" });\n// create a new boolean field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"My Field\", { Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-dependent-lookup-field","title":"Add a Dependent Lookup Field","text":"

Use the addDependentLookupField method to create a new dependent lookup field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst field = await sp.web.fields.addLookup(\"My Field\", { LookupListId: list.Id, LookupFieldName: \"Title\" });\n// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web.\nconst fieldDep = await sp.web.fields.addDependentLookupField(\"My Dep Field\", field.data.Id as string, \"Description\");\n// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", { LookupListId: list.Id, LookupFieldName: \"Title\" });\nconst fieldDep2 = await sp.web.lists.getByTitle(\"My List\").fields.addDependentLookupField(\"My Dep Field\", field2.data.Id as string, \"Description\");\n\n// we can use this 'fieldDep' variable to run more queries on the field:\nconst r = await fieldDep.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#add-a-location-field","title":"Add a Location Field","text":"

Use the addLocation method to create a new location field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// create a new location field called 'My Field' in web\nconst field = await sp.web.fields.addLocation(\"My Field\", { Group: \"My Group\" });\n// create a new location field called 'My Field' in list 'My List'\nconst field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLocation(\"My Field\", { Group: \"My Group\" });\n\n// we can use this 'field' variable to run more queries on the field:\nconst r = await field.field.select(\"Id\")();\n\n// log the field Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/fields/#delete-a-field","title":"Delete a Field","text":"

Use the delete method to delete a field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nawait sp.web.fields.addBoolean(\"Temp Field\", { Group: \"My Group\" });\nawait sp.web.fields.addBoolean(\"Temp Field 2\", { Group: \"My Group\" });\nawait sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"Temp Field\", { Group: \"My Group\" });\nawait sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"Temp Field 2\", { Group: \"My Group\" });\n\n// delete one or more fields from web, returns boolean\nconst result = await sp.web.fields.getByTitle(\"Temp Field\").delete();\nconst result2 = await sp.web.fields.getByTitle(\"Temp Field 2\").delete();\n\n\n// delete one or more fields from list 'My List', returns boolean\nconst result = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Temp Field\").delete();\nconst result2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Temp Field 2\").delete();\n
"},{"location":"sp/fields/#update-a-field","title":"Update a Field","text":"

Use the update method to update a field.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// update the field called 'My Field' with a description in web, returns FieldUpdateResult\nconst fieldUpdate = await sp.web.fields.getByTitle(\"My Field\").update({ Description: \"My Description\" });\n// update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult\nconst fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").update({ Description: \"My Description\" });\n\n// if you need to update a field with properties for a specific field type you can optionally include the field type as a second param\n// if you do not include it we will look up the type, but that adds a call to the server\nconst fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Look up Field\").update({ RelationshipDeleteBehavior: 1 }, \"SP.FieldLookup\");\n
"},{"location":"sp/fields/#show-a-field-in-the-display-form","title":"Show a Field in the Display Form","text":"

Use the setShowInDisplayForm method to add a field to the display form.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// show field called 'My Field' in display form throughout web\nawait sp.web.fields.getByTitle(\"My Field\").setShowInDisplayForm(true);\n// show field called 'My Field' in display form for list 'My List'\nawait sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInDisplayForm(true);\n
"},{"location":"sp/fields/#show-a-field-in-the-edit-form","title":"Show a Field in the Edit Form","text":"

Use the setShowInEditForm method to add a field to the edit form.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// show field called 'My Field' in edit form throughout web\nawait sp.web.fields.getByTitle(\"My Field\").setShowInEditForm(true);\n// show field called 'My Field' in edit form for list 'My List'\nawait sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInEditForm(true);\n
"},{"location":"sp/fields/#show-a-field-in-the-new-form","title":"Show a Field in the New Form","text":"

Use the setShowInNewForm method to add a field to the display form.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// show field called 'My Field' in new form throughout web\nawait sp.web.fields.getByTitle(\"My Field\").setShowInNewForm(true);\n// show field called 'My Field' in new form for list 'My List'\nawait sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInNewForm(true);\n
"},{"location":"sp/files/","title":"@pnp/sp/files","text":"

One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.

"},{"location":"sp/files/#reading-files","title":"Reading Files","text":"

Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst blob: Blob = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.avi\").getBlob();\n\nconst buffer: ArrayBuffer = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.avi\").getBuffer();\n\nconst json: any = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.json\").getJSON();\n\nconst text: string = await sp.web.getFileByServerRelativePath(\"/sites/dev/documents/file.txt\").getText();\n\n// all of these also work from a file object no matter how you access it\nconst text2: string = await sp.web.getFolderByServerRelativePath(\"/sites/dev/documents\").files.getByUrl(\"file.txt\").getText();\n
"},{"location":"sp/files/#getfilebyurl","title":"getFileByUrl","text":"

This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\n\nconst sp = spfi(...);\n\nconst url = \"{absolute file url OR sharing url}\";\n\n// file is an IFile and supports all the file operations\nconst file = sp.web.getFileByUrl(url);\n\n// for example\nconst fileContent = await file.getText();\n
"},{"location":"sp/files/#filefromserverrelativepath","title":"fileFromServerRelativePath","text":"

Added in 3.3.0

Utility method allowing you to get an IFile reference using any SPQueryable as a base and the server relative path to the file. Helpful when you do not have convenient access to an IWeb to use getFileByServerRelativePath.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { fileFromServerRelativePath } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst url = \"/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file = fileFromServerRelativePath(sp.web, url);\n\n// for example\nconst fileContent = await file.getText();\n
"},{"location":"sp/files/#filefromabsolutepath","title":"fileFromAbsolutePath","text":"

Added in 3.8.0

Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.

Works across site collections within the same tenant

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { fileFromAbsolutePath } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file = fileFromAbsolutePath(sp.web, url);\n\n// for example\nconst fileContent = await file.getText();\n
"},{"location":"sp/files/#filefrompath","title":"fileFromPath","text":"

Added in 3.8.0

Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute OR server relative path to the file.

Works across site collections within the same tenant

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { fileFromPath } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file = fileFromPath(sp.web, url);\n\n// for example\nconst fileContent = await file.getText();\n\nconst url2 = \"/sites/dev/documents/file.txt\";\n\n// file is an IFile and supports all the file operations\nconst file2 = fileFromPath(sp.web, url2);\n\n// for example\nconst fileContent2 = await file2.getText();\n
"},{"location":"sp/files/#adding-files","title":"Adding Files","text":"

Likewise you can add files using one of two methods, addUsingPath or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.

The addUsingPath method, supports the percent or pound characters in file names.

When using EnsureUniqueFileName property, you must omit the Overwrite parameter.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n//Sample uses pure JavaScript to access the input tag of type=\"file\" ->https://www.w3schools.com/tags/att_input_type_file.asp \nlet file = <HTMLInputElement>document.getElementById(\"thefileinput\");\nconst fileNamePath = encodeURI(file.name);\nlet result: IFileAddResult;\n// you can adjust this number to control what size files are uploaded in chunks\nif (file.size <= 10485760) {\n    // small upload\n    result = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files.addUsingPath(fileNamePath, file, { Overwrite: true });\n} else {\n    // large upload\n    result = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files.addChunked(fileNamePath, file, data => {\n    console.log(`progress`);\n    }, true);\n}\n\nconsole.log(`Result of file upload: ${JSON.stringify(result)}`);\n
"},{"location":"sp/files/#adding-a-file-using-nodejs-streams","title":"Adding a file using Nodejs Streams","text":"

If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams.

// triggers auto-application of extensions, in this case to add getStream\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/nodejs\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/folders/list\";\nimport \"@pnp/sp/files/folder\";\nimport { createReadStream } from 'fs';\n\n// get a stream of an existing file\nconst stream = createReadStream(\"c:/temp/file.txt\");\n\n// now add the stream as a new file\nconst sp = spfi(...);\n\nconst fr = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.addChunked( \"new.txt\", stream, undefined, true );\n
"},{"location":"sp/files/#setting-associated-item-values","title":"Setting Associated Item Values","text":"

You can also update the file properties of a newly uploaded file using code similar to the below snippet:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\nconst file = await sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared%20Documents/test/\").files.addUsingPath(\"file.name\", \"content\", {Overwrite: true});\nconst item = await file.file.getItem();\nawait item.update({\n  Title: \"A Title\",\n  OtherField: \"My Other Value\"\n});\n
"},{"location":"sp/files/#update-file-content","title":"Update File Content","text":"

You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/documents/test.txt\").setContent(\"New string content for the file.\");\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/documents/test.mp4\").setContentChunked(file);\n
"},{"location":"sp/files/#check-in-check-out-and-approve-deny","title":"Check in, Check out, and Approve & Deny","text":"

The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.

"},{"location":"sp/files/#check-in","title":"Check In","text":"

Check in takes two optional arguments, comment and check in type.

import { spfi } from \"@pnp/sp\";\nimport { CheckinType } from \"@pnp/sp/files\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// default options with empty comment and CheckinType.Major\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkin();\nconsole.log(\"File checked in!\");\n\n// supply a comment (< 1024 chars) and using default check in type CheckinType.Major\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\");\nconsole.log(\"File checked in!\");\n\n// Supply both comment and check in type\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\", CheckinType.Overwrite);\nconsole.log(\"File checked in!\");\n
"},{"location":"sp/files/#check-out","title":"Check Out","text":"

Check out takes no arguments.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nsp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").checkout();\nconsole.log(\"File checked out!\");\n
"},{"location":"sp/files/#approve-and-deny","title":"Approve and Deny","text":"

You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").approve(\"Approval Comment\");\nconsole.log(\"File approved!\");\n\n// deny with no comment\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").deny();\nconsole.log(\"File denied!\");\n\n// deny with a supplied comment.\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").deny(\"Deny comment\");\nconsole.log(\"File denied!\");\n
"},{"location":"sp/files/#publish-and-unpublish","title":"Publish and Unpublish","text":"

You can both publish and unpublish a file using the library. Both methods take an optional comment argument.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// publish with no comment\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").publish();\nconsole.log(\"File published!\");\n\n// publish with a supplied comment.\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").publish(\"Publish comment\");\nconsole.log(\"File published!\");\n\n// unpublish with no comment\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").unpublish();\nconsole.log(\"File unpublished!\");\n\n// unpublish with a supplied comment.\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/file.txt\").unpublish(\"Unpublish comment\");\nconsole.log(\"File unpublished!\");\n
"},{"location":"sp/files/#advanced-upload-options","title":"Advanced Upload Options","text":"

Both the addChunked and setContentChunked methods support options beyond just supplying the file content.

"},{"location":"sp/files/#progress-function","title":"progress function","text":"

A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:

(data: ChunkedFileUploadProgressData) => void

The data interface is:

export interface ChunkedFileUploadProgressData {\n    stage: \"starting\" | \"continue\" | \"finishing\";\n    blockNumber: number;\n    totalBlocks: number;\n    chunkSize: number;\n    currentPointer: number;\n    fileSize: number;\n}\n
"},{"location":"sp/files/#chunksize","title":"chunkSize","text":"

This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.

"},{"location":"sp/files/#getitem","title":"getItem","text":"

This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.

import { spFI, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem();\nconsole.log(item);\n\nconst item2 = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(\"Title\", \"Modified\");\nconsole.log(item2);\n\n// you can also chain directly off this item instance\nconst perms = await item.getCurrentUserEffectivePermissions();\nconsole.log(perms);\n

You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.

import { spFI, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\n// also supports typing the objects so your type will be a union type\nconst item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem<{ Id: number, Title: string }>(\"Id\", \"Title\");\n\n// You get intellisense and proper typing of the returned object\nconsole.log(`Id: ${item.Id} -- ${item.Title}`);\n\n// You can also chain directly off this item instance\nconst perms = await item.getCurrentUserEffectivePermissions();\nconsole.log(perms);\n
"},{"location":"sp/files/#move-by-path","title":"move by path","text":"

It's possible to move a file to a new destination within a site collection

If you change the filename during the move operation this is considered an \"edit\" and the file's modified information will be updated regardless of the \"RetainEditorAndModifiedOnMove\" setting.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveByPath(destinationUrl, false, true);\n

Added in 3.7.0

You can also supply a set of detailed options to better control the move process:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/new-file.docx\").moveByPath(destinationUrl, false, {\n    KeepBoth: false,\n    RetainEditorAndModifiedOnMove: true,\n    ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/files/#copy","title":"copy","text":"

It's possible to copy a file to a new destination within a site collection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyTo(destinationUrl, false);\n
"},{"location":"sp/files/#copy-by-path","title":"copy by path","text":"

It's possible to copy a file to a new destination within the same or a different site collection.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, true);\n

Added in 3.7.0

You can also supply a set of detailed options to better control the copy process:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;\n\nawait sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, {\n    KeepBoth: false,\n    ResetAuthorAndCreatedOnCopy: true,\n    ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/files/#getfilebyid","title":"getFileById","text":"

You can get a file by Id from a web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\nimport { IFile } from \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nconst file: IFile = sp.web.getFileById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");\n
"},{"location":"sp/files/#delete","title":"delete","text":"

Deletes a file

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nawait sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"filename.txt\").delete();\n
"},{"location":"sp/files/#delete-with-params","title":"delete with params","text":"

Deletes a file with options

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nawait sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"filename.txt\").deleteWithParams({\n    BypassSharedLock: true,\n});\n
"},{"location":"sp/files/#exists","title":"exists","text":"

Checks to see if a file exists

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nconst exists = await sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"name.txt\").exists();\n
"},{"location":"sp/files/#lockedbyuser","title":"lockedByUser","text":"

Gets the user who currently has this file locked for shared use

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\nconst user = await sp.web.getFolderByServerRelativePath(\"{folder relative path}\").files.getByUrl(\"name.txt\").getLockedByUser();\n
"},{"location":"sp/folders/","title":"@pnp/sp/folders","text":"

Folders serve as a container for your files and list items.

"},{"location":"sp/folders/#ifolders","title":"IFolders","text":"

Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties.

"},{"location":"sp/folders/#get-folders-collection-for-various-sharepoint-objects","title":"Get folders collection for various SharePoint objects","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// gets web's folders\nconst webFolders = await sp.web.folders();\n\n// gets list's folders\nconst listFolders = await sp.web.lists.getByTitle(\"My List\").rootFolder.folders();\n\n// gets item's folders\nconst itemFolders = await sp.web.lists.getByTitle(\"My List\").items.getById(1).folder.folders();\n
"},{"location":"sp/folders/#folderfromserverrelativepath","title":"folderFromServerRelativePath","text":"

Added in 3.3.0

Utility method allowing you to get an IFolder reference using any SPQueryable as a base and the server relative path to the folder. Helpful when you do not have convenient access to an IWeb to use getFolderByServerRelativePath.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { folderFromServerRelativePath } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst url = \"/sites/dev/documents/folder4\";\n\n// file is an IFile and supports all the file operations\nconst folder = folderFromServerRelativePath(sp.web, url);\n
"},{"location":"sp/folders/#folderfromabsolutepath","title":"folderFromAbsolutePath","text":"

Added in 3.8.0

Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.

Works across site collections within the same tenant

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { folderFromAbsolutePath } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/folder\";\n\n// file is an IFile and supports all the file operations\nconst folder = folderFromAbsolutePath(sp.web, url);\n\n// for example\nconst folderInfo = await folder();\n
"},{"location":"sp/folders/#folderfrompath","title":"folderFromPath","text":"

Added in 3.8.0

Utility method allowing you to get an IFolder reference using any SPQueryable as a base and an absolute OR server relative path to the file.

Works across site collections within the same tenant

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { folderFromPath } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst url = \"https://tenant.sharepoint.com/sites/dev/documents/folder\";\n\n// file is an IFile and supports all the file operations\nconst folder = folderFromPath(sp.web, url);\n\n// for example\nconst folderInfo = await folder();\n\nconst url2 = \"/sites/dev/documents/folder\";\n\n// file is an IFile and supports all the file operations\nconst folder2 = folderFromPath(sp.web, url2);\n\n// for example\nconst folderInfo2 = await folder2();\n
"},{"location":"sp/folders/#add","title":"add","text":"

Adds a new folder to collection of folders

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// creates a new folder for web with specified url\nconst folderAddResult = await sp.web.folders.addUsingPath(\"folder url\");\n
"},{"location":"sp/folders/#getbyurl","title":"getByUrl","text":"

Gets a folder instance from a collection by folder's name

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder = await sp.web.folders.getByUrl(\"folder name\")();\n
"},{"location":"sp/folders/#ifolder","title":"IFolder","text":"

Represents an instance of a SharePoint folder.

"},{"location":"sp/folders/#get-a-folder-object-associated-with-different-sharepoint-artifacts-web-list-list-item","title":"Get a folder object associated with different SharePoint artifacts (web, list, list item)","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// web's folder\nconst rootFolder = await sp.web.rootFolder();\n\n// list's folder\nconst listRootFolder = await sp.web.lists.getByTitle(\"234\").rootFolder();\n\n// item's folder\nconst itemFolder = await sp.web.lists.getByTitle(\"234\").items.getById(1).folder();\n
"},{"location":"sp/folders/#getitem","title":"getItem","text":"

Gets list item associated with a folder

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folderItem = await sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").folders.getByUrl(\"My Folder\").getItem();\n
"},{"location":"sp/folders/#storagemetrics","title":"storageMetrics","text":"

Added in 3.8.0

Gets a set of metrics describing the total file size contained in the folder.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst metrics = await sp.web.getFolderByServerRelativePath(\"/sites/dev/shared documents/target\").storageMetrics();\n\n// you can also select specific metrics if desired:\nconst metrics2 = await sp.web.getFolderByServerRelativePath(\"/sites/dev/shared documents/target\").storageMetrics.select(\"TotalSize\")();\n
"},{"location":"sp/folders/#move-by-path","title":"move by path","text":"

It's possible to move a folder to a new destination within the same or a different site collection

If you change the filename during the move operation this is considered an \"edit\" and the file's modified information will be updated regardless of the \"RetainEditorAndModifiedOnMove\" setting.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new folder\nconst destinationUrl = `/sites/my-site/SiteAssets/new-folder`;\n\nawait sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").folders.getByUrl(\"My Folder\").moveByPath(destinationUrl, true);\n

Added in 3.8.0

You can also supply a set of detailed options to better control the move process:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/folder`;\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents/folder\").moveByPath(destinationUrl, {\n    KeepBoth: false,\n    RetainEditorAndModifiedOnMove: true,\n    ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/folders/#copy-by-path","title":"copy by path","text":"

It's possible to copy a folder to a new destination within the same or a different site collection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new folder\nconst destinationUrl = `/sites/my-site/SiteAssets/new-folder`;\n\nawait sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").folders.getByUrl(\"My Folder\").copyByPath(destinationUrl, true);\n

Added in 3.8.0

You can also supply a set of detailed options to better control the copy process:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// destination is a server-relative url of a new file\nconst destinationUrl = `/sites/dev2/SiteAssets/folder`;\n\nawait sp.web.getFolderByServerRelativePath(\"/sites/dev/Shared Documents/folder\").copyByPath(destinationUrl, false, {\n    KeepBoth: false,\n    ResetAuthorAndCreatedOnCopy: true,\n    ShouldBypassSharedLocks: false,\n});\n
"},{"location":"sp/folders/#delete","title":"delete","text":"

Deletes a folder

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.rootFolder.folders.getByUrl(\"My Folder\").delete();\n
"},{"location":"sp/folders/#delete-with-params","title":"delete with params","text":"

Deletes a folder with options

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.rootFolder.folders.getByUrl(\"My Folder\").deleteWithParams({\n                BypassSharedLock: true,\n                DeleteIfEmpty: true,\n            });\n
"},{"location":"sp/folders/#recycle","title":"recycle","text":"

Recycles a folder

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.rootFolder.folders.getByUrl(\"My Folder\").recycle();\n
"},{"location":"sp/folders/#serverrelativeurl","title":"serverRelativeUrl","text":"

Gets folder's server relative url

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst relUrl = await sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").select('ServerRelativeUrl')();\n
"},{"location":"sp/folders/#update","title":"update","text":"

Updates folder's properties

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").update({\n        \"Name\": \"New name\",\n    });\n
"},{"location":"sp/folders/#contenttypeorder","title":"contentTypeOrder","text":"

Gets content type order of a folder

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst order = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").select('contentTypeOrder')();\n
"},{"location":"sp/folders/#folders","title":"folders","text":"

Gets all child folders associated with the current folder

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folders = await sp.web.rootFolder.folders();\n
"},{"location":"sp/folders/#files","title":"files","text":"

Gets all files inside a folder

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/files/folder\";\n\nconst sp = spfi(...);\n\nconst files = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files();\n
"},{"location":"sp/folders/#listitemallfields","title":"listItemAllFields","text":"

Gets this folder's list item field values

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst itemFields = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").listItemAllFields();\n
"},{"location":"sp/folders/#parentfolder","title":"parentFolder","text":"

Gets the parent folder, if available

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst parentFolder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").parentFolder();\n
"},{"location":"sp/folders/#properties","title":"properties","text":"

Gets this folder's properties

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst properties = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").properties();\n
"},{"location":"sp/folders/#uniquecontenttypeorder","title":"uniqueContentTypeOrder","text":"

Gets a value that specifies the content type order.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst contentTypeOrder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").select('uniqueContentTypeOrder')();\n
"},{"location":"sp/folders/#rename-a-folder","title":"Rename a folder","text":"

You can rename a folder by updating FileLeafRef property:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder = sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\");\n\nconst item = await folder.getItem();\nconst result = await item.update({ FileLeafRef: \"Folder2\" });\n
"},{"location":"sp/folders/#create-a-folder-with-custom-content-type","title":"Create a folder with custom content type","text":"

Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\nconst newFolderResult = await sp.web.rootFolder.folders.getByUrl(\"Shared Documents\").folders.addUsingPath(\"My New Folder\");\nconst item = await newFolderResult.folder.listItemAllFields();\n\nawait sp.web.lists.getByTitle(\"Documents\").items.getById(item.ID).update({\n    ContentTypeId: \"0x0120001E76ED75A3E3F3408811F0BF56C4CDDD\",\n    MyFolderField: \"field value\",\n    Title: \"My New Folder\",\n});\n
"},{"location":"sp/folders/#addsubfolderusingpath","title":"addSubFolderUsingPath","text":"

You can use the addSubFolderUsingPath method to add a folder with some special chars supported

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport { IFolder } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\n// add a folder to site assets\nconst folder: IFolder = await sp.web.rootFolder.folders.getByUrl(\"SiteAssets\").addSubFolderUsingPath(\"folder name\");\n
"},{"location":"sp/folders/#getfolderbyid","title":"getFolderById","text":"

You can get a folder by Id from a web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\nimport { IFolder } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");\n
"},{"location":"sp/folders/#getparentinfos","title":"getParentInfos","text":"

Gets information about folder, including details about the parent list, parent list root folder, and parent web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");\nawait folder.getParentInfos();\n
"},{"location":"sp/forms/","title":"@pnp/sp/forms","text":"

Forms in SharePoint are the Display, New, and Edit forms associated with a list.

"},{"location":"sp/forms/#iforms","title":"IForms","text":""},{"location":"sp/forms/#get-form-by-id","title":"Get Form by Id","text":"

Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/forms\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// get the field by Id for web\nconst form = sp.web.lists.getByTitle(\"Documents\").forms.getById(\"{c4486774-f1e2-4804-96f3-91edf3e22a19}\")();\n
"},{"location":"sp/groupSiteManager/","title":"GroupSiteManager","text":""},{"location":"sp/groupSiteManager/#pnpspgroupsitemanager","title":"@pnp/sp/groupsitemanager","text":"

The @pnp/sp/groupsitemanager package represents calls to _api/groupsitemanager endpoint and is accessible from any site url.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/groupsitemanager\";\n\nconst sp = spfi(...);\n\n// call method to check if the current user can create Microsoft 365 groups\nconst isUserAllowed = await sp.groupSiteManager.canUserCreateGroup();\n\n// call method to delete a group-connected site\nawait sp.groupSiteManager.delete(\"https://contoso.sharepoint.com/sites/hrteam\");\n\n//call method to gets labels configured for the tenant\nconst orgLabels = await sp.groupSiteManager.getAllOrgLabels(0);\n\n//call method to get information regarding site groupification configuration for the current site context\nconst groupCreationContext = await sp.groupSiteManager.getGroupCreationContext();\n\n//call method to get information regarding site groupification configuration for the current site context\nconst siteData = await sp.groupSiteManager.getGroupSiteConversionData();\n\n// call method to get teams membership for a user\nconst userTeams = await sp.groupSiteManager.getUserTeamConnectedMemberGroups(\"meganb@contoso.onmicrosoft.com\");\n\n// call method to get shared channel memberhsip for user\nconst sharedChannels = await sp.groupSiteManager.getUserSharedChannelMemberGroups(\"meganb@contoso.onmicrosoft.com\");\n\n//call method to get valid site url from Alias\nconst siteUrl = await sp.groupSiteManager.getValidSiteUrlFromAlias(\"contoso\");\n\n//call method to check if teamify prompt is hidden\nconst isTeamifyPromptHidden = await sp.groupSiteManager.isTeamifyPromptHidden(\"https://contoso.sharepoint.com/sites/hrteam\");\n

For more information on the methods available and how to use them, please review the code comments in the source.

"},{"location":"sp/hubsites/","title":"@pnp/sp/hubsites","text":"

This module helps you with working with hub sites in your tenant.

"},{"location":"sp/hubsites/#ihubsites","title":"IHubSites","text":""},{"location":"sp/hubsites/#get-a-listing-of-all-hub-sites","title":"Get a Listing of All Hub sites","text":"
import { spfi } from \"@pnp/sp\";\nimport { IHubSiteInfo } from  \"@pnp/sp/hubsites\";\nimport \"@pnp/sp/hubsites\";\n\nconst sp = spfi(...);\n\n// invoke the hub sites object\nconst hubsites: IHubSiteInfo[] = await sp.hubSites();\n\n// you can also use select to only return certain fields:\nconst hubsites2: IHubSiteInfo[] = await sp.hubSites.select(\"ID\", \"Title\", \"RelatedHubSiteIds\")();\n
"},{"location":"sp/hubsites/#get-hub-site-by-id","title":"Get Hub site by Id","text":"

Using the getById method on the hubsites module to get a hub site by site Id (guid).

import { spfi } from \"@pnp/sp\";\nimport { IHubSiteInfo } from  \"@pnp/sp/hubsites\";\nimport \"@pnp/sp/hubsites\";\n\nconst sp = spfi(...);\n\nconst hubsite: IHubSiteInfo = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\")();\n\n// log hub site title to console\nconsole.log(hubsite.Title);\n
"},{"location":"sp/hubsites/#get-isite-instance","title":"Get ISite instance","text":"

We provide a helper method to load the ISite instance from the HubSite

import { spfi } from \"@pnp/sp\";\nimport { ISite } from  \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites\";\n\nconst sp = spfi(...);\n\nconst site: ISite = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\").getSite();\n\nconst siteData = await site();\n\nconsole.log(siteData.Title);\n
"},{"location":"sp/hubsites/#get-hub-site-data-for-a-web","title":"Get Hub site data for a web","text":"
import { spfi } from \"@pnp/sp\";\nimport { IHubSiteWebData } from  \"@pnp/sp/hubsites\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\n\nconst webData: Partial<IHubSiteWebData> = await sp.web.hubSiteData();\n\n// you can also force a refresh of the hub site data\nconst webData2: Partial<IHubSiteWebData> = await sp.web.hubSiteData(true);\n
"},{"location":"sp/hubsites/#synchubsitetheme","title":"syncHubSiteTheme","text":"

Allows you to apply theme updates from the parent hub site collection.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\n\nawait sp.web.syncHubSiteTheme();\n
"},{"location":"sp/hubsites/#hub-site-site-methods","title":"Hub site Site Methods","text":"

You manage hub sites at the Site level.

"},{"location":"sp/hubsites/#joinhubsite","title":"joinHubSite","text":"

Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites/site\";\n\nconst sp = spfi(...);\n\n// join a site to a hub site\nawait sp.site.joinHubSite(\"{parent hub site id}\");\n\n// remove a site from a hub site\nawait sp.site.joinHubSite(\"00000000-0000-0000-0000-000000000000\");\n
"},{"location":"sp/hubsites/#registerhubsite","title":"registerHubSite","text":"

Registers the current site collection as hub site collection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites/site\";\n\nconst sp = spfi(...);\n\n// register current site as a hub site\nawait sp.site.registerHubSite();\n
"},{"location":"sp/hubsites/#unregisterhubsite","title":"unRegisterHubSite","text":"

Un-registers the current site collection as hub site collection.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/hubsites/site\";\n\nconst sp = spfi(...);\n\n// make a site no longer a hub\nawait sp.site.unRegisterHubSite();\n
"},{"location":"sp/items/","title":"@pnp/sp/items","text":""},{"location":"sp/items/#get","title":"GET","text":"

Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.

"},{"location":"sp/items/#basic-get","title":"Basic Get","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// get all the items from a list\nconst items: any[] = await sp.web.lists.getByTitle(\"My List\").items();\nconsole.log(items);\n\n// get a specific item by id.\nconst item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)();\nconsole.log(item);\n\n// use odata operators for more efficient queries\nconst items2: any[] = await sp.web.lists.getByTitle(\"My List\").items.select(\"Title\", \"Description\").top(5).orderBy(\"Modified\", true)();\nconsole.log(items2);\n
"},{"location":"sp/items/#get-paged-items","title":"Get Paged Items","text":"

Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// basic case to get paged items form a list\nconst items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged();\n\n// you can also provide a type for the returned values instead of any\nconst items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged<{Title: string}[]>();\n\n// the query also works with select to choose certain fields and top to set the page size\nconst items = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\", \"Description\").top(50).getPaged<{Title: string}[]>();\n\n// the results object will have two properties and one method:\n\n// the results property will be an array of the items returned\nif (items.results.length > 0) {\n    console.log(\"We got results!\");\n\n    for (let i = 0; i < items.results.length; i++) {\n        // type checking works here if we specify the return type\n        console.log(items.results[i].Title);\n    }\n}\n\n// the hasNext property is used with the getNext method to handle paging\n// hasNext will be true so long as there are additional results\nif (items.hasNext) {\n\n    // this will carry over the type specified in the original query for the results array\n    items = await items.getNext();\n    console.log(items.results.length);\n}\n
"},{"location":"sp/items/#getlistitemchangessincetoken","title":"getListItemChangesSinceToken","text":"

The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// Using RowLimit. Enables paging\nconst changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({RowLimit: '5'});\n\n// Use QueryOptions to make a XML-style query.\n// Because it's XML we need to escape special characters\n// Instead of & we use &amp; in the query\nconst changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext=\"Paged=TRUE&amp;p_ID=5\" />'});\n\n// Get everything. Using null with ChangeToken gets everything\nconst changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({ChangeToken: null});\n\n
"},{"location":"sp/items/#get-all-items","title":"Get All Items","text":"

Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used.

In v3 there is a separate import for get-all to include the functionality. This is to remove the code from bundles for folks who do not need it.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/items/get-all\";\n\nconst sp = spfi(...);\n\n// basic usage\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll();\nconsole.log(allItems.length);\n\n// set page size\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(4000);\nconsole.log(allItems.length);\n\n// use select and top. top will set page size and override the any value passed to getAll\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").top(4000).getAll();\nconsole.log(allItems.length);\n\n// we can also use filter as a supported odata operation, but this will likely fail on large lists\nconst allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").filter(\"Title eq 'Test'\").getAll();\nconsole.log(allItems.length);\n
"},{"location":"sp/items/#retrieving-lookup-fields","title":"Retrieving Lookup Fields","text":"

When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst items = await sp.web.lists.getByTitle(\"LookupList\").items.select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")();\nconsole.log(items);\n\nconst item = await sp.web.lists.getByTitle(\"LookupList\").items.getById(1).select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")();\nconsole.log(item);\n
"},{"location":"sp/items/#filter-using-metadata-fields","title":"Filter using Metadata fields","text":"

To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\n\nconst sp = spfi(...);\n\nconst r = await sp.web.lists.getByTitle(\"TaxonomyList\").getItemsByCAMLQuery({\n    ViewXml: `<View><Query><Where><Eq><FieldRef Name=\"MetaData\"/><Value Type=\"TaxonomyFieldType\">Term 2</Value></Eq></Where></Query></View>`,\n});\n
"},{"location":"sp/items/#retrieving-publishingpageimage","title":"Retrieving PublishingPageImage","text":"

The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { Web } from \"@pnp/sp/webs\";\n\ntry {\n  const sp = spfi(\"https://{publishing site url}\").using(SPFx(this.context));\n\n  const r = await sp.web.lists.getByTitle(\"Pages\").items\n    .select(\"Title\", \"FileRef\", \"FieldValuesAsText/MetaInfo\")\n    .expand(\"FieldValuesAsText\")\n    ();\n\n  // look through the returned items.\n  for (var i = 0; i < r.length; i++) {\n\n    // the title field value\n    console.log(r[i].Title);\n\n    // find the value in the MetaInfo string using regex\n    const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig.exec(r[i].FieldValuesAsText.MetaInfo);\n    if (matches !== null && matches.length > 1) {\n\n      // this wil be the value of the PublishingPageImage field\n      console.log(matches[1]);\n    }\n  }\n}\ncatch (e) {\n  console.error(e);\n}\n
"},{"location":"sp/items/#add-items","title":"Add Items","text":"

There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { IItemAddResult } from \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// add an item to the list\nconst iar: IItemAddResult = await sp.web.lists.getByTitle(\"My List\").items.add({\n  Title: \"Title\",\n  Description: \"Description\"\n});\n\nconsole.log(iar);\n
"},{"location":"sp/items/#content-type","title":"Content Type","text":"

You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getById(\"4D5A36EA-6E84-4160-8458-65C436DB765C\").items.add({\n    Title: \"Test 1\",\n    ContentTypeId: \"0x01030058FD86C279252341AB303852303E4DAF\"\n});\n
"},{"location":"sp/items/#user-fields","title":"User Fields","text":"

There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.

Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an array. Examples for both are shown below.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { getGUID } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\nconst i = await sp.web.lists.getByTitle(\"PeopleFields\").items.add({\n  Title: getGUID(),\n  User1Id: 9, // allows a single user\n  User2Id: [16, 45] // allows multiple users\n});\n\nconsole.log(i);\n

If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.lists.getByTitle(\"UserFieldList\").items.getById(1).validateUpdateListItem([{\n    FieldName: \"UserField\",\n    FieldValue: JSON.stringify([{ \"Key\": \"i:0#.f|membership|person@tenant.com\" }]),\n},\n{\n    FieldName: \"Title\",\n    FieldValue: \"Test - Updated\",\n}]);\n
"},{"location":"sp/items/#lookup-fields","title":"Lookup Fields","text":"

What is said for User Fields is, in general, relevant to Lookup Fields:

  • Lookup Field types:
  • Single-valued lookup
  • Multiple-valued lookup
  • Id suffix should be appended to the end of lookups EntityPropertyName in payloads
  • Numeric Ids for lookups' items should be passed as values
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport { getGUID } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"LookupFields\").items.add({\n    Title: getGUID(),\n    LookupFieldId: 2,       // allows a single lookup value\n    MultiLookupFieldId: [1, 56]  // allows multiple lookup value\n});\n
"},{"location":"sp/items/#add-multiple-items","title":"Add Multiple Items","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/batching\";\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nconst list = batchedSP.web.lists.getByTitle(\"rapidadd\");\n\nlet res = [];\n\nlist.items.add({ Title: \"Batch 6\" }).then(r => res.push(r));\n\nlist.items.add({ Title: \"Batch 7\" }).then(r => res.push(r));\n\n// Executes the batched calls\nawait execute();\n\n// Results for all batched calls are available\nfor(let i = 0; i < res.length; i++) {\n    ///Do something with the results\n}\n
"},{"location":"sp/items/#update-items","title":"Update Items","text":"

The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.

Note: For updating certain types of fields, see the Add examples above. The payload will be the same you will just need to replace the .add method with .getById({itemId}).update.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nconst i = await list.items.getById(1).update({\n  Title: \"My New Title\",\n  Description: \"Here is a new description\"\n});\n\nconsole.log(i);\n
"},{"location":"sp/items/#getting-and-updating-a-collection-using-filter","title":"Getting and updating a collection using filter","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// you are getting back a collection here\nconst items: any[] = await sp.web.lists.getByTitle(\"MyList\").items.top(1).filter(\"Title eq 'A Title'\")();\n\n// see if we got something\nif (items.length > 0) {\n  const updatedItem = await sp.web.lists.getByTitle(\"MyList\").items.getById(items[0].Id).update({\n    Title: \"Updated Title\",\n  });\n\n  console.log(JSON.stringify(updatedItem));\n}\n
"},{"location":"sp/items/#update-multiple-items","title":"Update Multiple Items","text":"

This approach avoids multiple calls for the same list's entity type name.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/batching\"\n\nconst sp = spfi(...);\n\nconst [batchedSP, execute] = sp.batched();\n\nconst list = batchedSP.web.lists.getByTitle(\"rapidupdate\");\n\nlist.items.getById(1).update({ Title: \"Batch 6\" }).then(b => {\n  console.log(b);\n});\n\nlist.items.getById(2).update({ Title: \"Batch 7\" }).then(b => {\n  console.log(b);\n});\n\n// Executes the batched calls\nawait execute();\n\nconsole.log(\"Done\");\n
"},{"location":"sp/items/#update-taxonomy-field","title":"Update Taxonomy field","text":"

Note: Updating Taxonomy field for a File item should be handled differently. Instead of using update(), use validateUpdateListItem(). Please see below

List Item

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"Demo\").items.getById(1).update({\n    MetaDataColumn: { Label: \"Demo\", TermGuid: '883e4c81-e8f9-4f19-b90b-6ab805c9f626', WssId: '-1' }\n});\n\n

File List Item

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\nawait (await sp.web.getFileByServerRelativePath(\"/sites/demo/DemoLibrary/File.txt\").getItem()).validateUpdateListItem([{\n    FieldName: \"MetaDataColumn\",\n    FieldValue:\"Demo|883e4c81-e8f9-4f19-b90b-6ab805c9f626\", //Label|TermGuid\n}]);\n
"},{"location":"sp/items/#update-multi-value-taxonomy-field","title":"Update Multi-value Taxonomy field","text":"

Based on this excellent article from Beau Cameron.

As he says you must update a hidden field to get this to work via REST. My meta data field accepting multiple values is called \"MultiMetaData\".

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\n// first we need to get the hidden field's internal name.\n// The Title of that hidden field is, in my case and in the linked article just the visible field name with \"_0\" appended.\nconst fields = await sp.web.lists.getByTitle(\"TestList\").fields.filter(\"Title eq 'MultiMetaData_0'\").select(\"Title\", \"InternalName\")();\n// get an item to update, here we just create one for testing\nconst newItem = await sp.web.lists.getByTitle(\"TestList\").items.add({\n  Title: \"Testing\",\n});\n// now we have to create an update object\n// to do that for each field value you need to serialize each as -1;#{field label}|{field id} joined by \";#\"\n// update with the values you want, this also works in the add call directly to avoid a second call\nconst updateVal = {};\nupdateVal[fields[0].InternalName] = \"-1;#New Term|bb046161-49cc-41bd-a459-5667175920d4;#-1;#New 2|0069972e-67f1-4c5e-99b6-24ac5c90b7c9\";\n// execute the update call\nawait newItem.item.update(updateVal);\n
"},{"location":"sp/items/#update-bcs-field","title":"Update BCS Field","text":"

Please see the issue for full details.

You will need to use validateUpdateListItem to ensure hte BCS field is updated correctly.

const update = await sp.web.lists.getByTitle(\"Price\").items.getById(7).select('*,External').validateUpdateListItem([\n      {FieldName:\"External\",FieldValue:\"Fauntleroy Circus\"},\n      {FieldName:\"Customers_ID\", FieldValue:\"__bk410024003500240054006500\"}\n    ]); \n
"},{"location":"sp/items/#recycle","title":"Recycle","text":"

To send an item to the recycle bin use recycle.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nconst recycleBinIdentifier = await list.items.getById(1).recycle();\n
"},{"location":"sp/items/#delete","title":"Delete","text":"

Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nawait list.items.getById(1).delete();\n
"},{"location":"sp/items/#delete-with-params","title":"Delete With Params","text":"

Deletes the item object with options.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"MyList\");\n\nawait list.items.getById(1).deleteWithParams({\n                BypassSharedLock: true,\n            });\n

The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true

"},{"location":"sp/items/#resolving-field-names","title":"Resolving field names","text":"

It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used.

The easiest way to get know EntityPropertyName is to use the following snippet:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/fields\";\n\nconst sp = spfi(...);\n\nconst response =\n  await sp.web.lists\n    .getByTitle('[Lists_Title]')\n    .fields\n    .select('Title, EntityPropertyName')\n    .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`)\n    ();\n\nconsole.log(response.map(field => {\n  return {\n    Title: field.Title,\n    EntityPropertyName: field.EntityPropertyName\n  };\n}));\n

Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.

"},{"location":"sp/items/#getparentinfos","title":"getParentInfos","text":"

Gets information about an item, including details about the parent list, parent list root folder, and parent web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\nconst item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)();\nawait item.getParentInfos();\n
"},{"location":"sp/lists/","title":"@pnp/sp/lists","text":"

Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.

"},{"location":"sp/lists/#ilists","title":"ILists","text":""},{"location":"sp/lists/#get-list-by-id","title":"Get List by Id","text":"

Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// get the list by Id\nconst list = sp.web.lists.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\");\n\n// we can use this 'list' variable to execute more queries on the list:\nconst r = await list.select(\"Title\")();\n\n// show the response from the server\nconsole.log(r.Title);\n
"},{"location":"sp/lists/#get-list-by-title","title":"Get List by Title","text":"

You can also get a list from the collection by title.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// get the default document library 'Documents'\nconst list = sp.web.lists.getByTitle(\"Documents\");\n\n// we can use this 'list' variable to run more queries on the list:\nconst r = await list.select(\"Id\")();\n\n// log the list Id to console\nconsole.log(r.Id);\n
"},{"location":"sp/lists/#add-list","title":"Add List","text":"

You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\n// create a new list, passing only the title\nconst listAddResult = await sp.web.lists.add(\"My new list\");\n\n// we can work with the list created using the IListAddResult.list property:\nconst r = await listAddResult.list.select(\"Title\")();\n\n// log newly created list title to console\nconsole.log(r.Title);\n});\n

You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs.

// this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings)\nconst listAddResult = await sp.web.lists.add(\"My Doc Library\", \"This is a description of doc lib.\", 101, true, { OnQuickLaunch: true });\n\n// get the Id of the newly added document library\nconst r = await listAddResult.list.select(\"Id\")();\n\n// log id to console\nconsole.log(r.Id);\n
"},{"location":"sp/lists/#ensure-that-a-list-exists-by-title","title":"Ensure that a List exists (by title)","text":"

Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default):\nconst listEnsureResult = await sp.web.lists.ensure(\"My List\");\n\n// check if the list was created, or if it already existed:\nif (listEnsureResult.created) {\n    console.log(\"My List was created!\");\n} else {\n    console.log(\"My List already existed!\");\n}\n\n// work on the created/updated list\nconst r = await listEnsureResult.list.select(\"Id\")();\n\n// log the Id\nconsole.log(r.Id);\n

If the list already exists, the other settings you provide will be used to update the existing list.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// add a new list to the lists collection of the web\nsp.web.lists.add(\"My List 2\").then(async () => {\n\n// then call ensure on the created list with an updated description\nconst listEnsureResult = await sp.web.lists.ensure(\"My List 2\", \"Updated description\");\n\n// get the updated description\nconst r = await listEnsureResult.list.select(\"Description\")();\n\n// log the updated description\nconsole.log(r.Description);\n});\n
"},{"location":"sp/lists/#ensure-site-assets-library-exist","title":"Ensure Site Assets Library exist","text":"

Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// get Site Assets library\nconst siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary();\n\n// get the Title\nconst r = await siteAssetsList.select(\"Title\")();\n\n// log Title\nconsole.log(r.Title);\n
"},{"location":"sp/lists/#ensure-site-pages-library-exist","title":"Ensure Site Pages Library exist","text":"

Gets a list that is the default location for wiki pages.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n// get Site Pages library\nconst siteAssetsList = await sp.web.lists.ensureSitePagesLibrary();\n\n// get the Title\nconst r = await siteAssetsList.select(\"Title\")();\n\n// log Title\nconsole.log(r.Title);\n
"},{"location":"sp/lists/#ilist","title":"IList","text":"Scenario Import Statement Selective 1 import { List, IList } from \"@pnp/sp/lists\"; Selective 2 import \"@pnp/sp/lists\"; Preset: All import { sp, List, IList } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, List, IList } from \"@pnp/sp/presets/core\";"},{"location":"sp/lists/#update-a-list","title":"Update a list","text":"

Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is \"*\")

import { IListUpdateResult } from \"@pnp/sp/lists\";\n\n// create a TypedHash object with the properties to update\nconst updateProperties = {\n    Description: \"This list title and description has been updated using PnPjs.\",\n    Title: \"Updated title\",\n};\n\n// update the list with the properties above\nlist.update(updateProperties).then(async (l: IListUpdateResult) => {\n\n    // get the updated title and description\n    const r = await l.list.select(\"Title\", \"Description\")();\n\n    // log the updated properties to the console\n    console.log(r.Title);\n    console.log(r.Description);\n});\n
"},{"location":"sp/lists/#get-changes-on-a-list","title":"Get changes on a list","text":"

From the change log, you can get a collection of changes that have occurred within the list based on the specified query.

import { IChangeQuery } from \"@pnp/sp\";\n\n// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore\nconst changeQuery: IChangeQuery = {\n    Add: true,\n    ChangeTokenEnd: null,\n    ChangeTokenStart: null,\n    DeleteObject: true,\n    Rename: true,\n    Restore: true,\n};\n\n// get list changes\nconst r = await list.getChanges(changeQuery);\n\n// log changes to console\nconsole.log(r);\n

To get changes from a specific time range you can use the ChangeTokenStart or a combination of ChangeTokenStart and ChangeTokenEnd.

import { IChangeQuery } from \"@pnp/sp\";\n\n//Resource is the list Id (as Guid)\nconst resource = list.Id;\nconst changeStart = new Date(\"2022-02-22\").getTime();\nconst changeTokenStart = `1;3;${resource};${changeStart};-1`;\n\n// build the changeQuery object, here we look at changes regarding Add and Update for Items.\nconst changeQuery: IChangeQuery = {\n    Add: true,\n    Update: true,\n    Item: true,\n    ChangeTokenEnd: null,\n    ChangeTokenStart: { StringValue: changeTokenStart },\n};\n\n// get list changes\nconst r = await list.getChanges(changeQuery);\n\n// log changes to console\nconsole.log(r);\n
"},{"location":"sp/lists/#get-list-items-using-a-caml-query","title":"Get list items using a CAML Query","text":"

You can get items from SharePoint using a CAML Query.

import { ICamlQuery } from \"@pnp/sp/lists\";\n\n// build the caml query object (in this example, we include Title field and limit rows to 5)\nconst caml: ICamlQuery = {\n    ViewXml: \"<View><ViewFields><FieldRef Name='Title' /></ViewFields><RowLimit>5</RowLimit></View>\",\n};\n\n// get list items\nconst r = await list.getItemsByCAMLQuery(caml);\n\n// log resulting array to console\nconsole.log(r);\n

If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment)

import { ICamlQuery } from \"@pnp/sp/lists\";\n\n// build the caml query object (in this example, we include Title field and limit rows to 5)\nconst caml: ICamlQuery = {\n    ViewXml: \"<View><ViewFields><FieldRef Name='Title' /><FieldRef Name='RoleAssignments' /></ViewFields><RowLimit>5</RowLimit></View>\",\n};\n\n// get list items\nconst r = await list.getItemsByCAMLQuery(caml, \"RoleAssignments\");\n\n// log resulting item array to console\nconsole.log(r);\n
"},{"location":"sp/lists/#get-list-items-changes-using-a-token","title":"Get list items changes using a Token","text":"
import {  IChangeLogItemQuery } from \"@pnp/sp/lists\";\n\n// build the caml query object (in this example, we include Title field and limit rows to 5)\nconst changeLogItemQuery: IChangeLogItemQuery = {\n    Contains: `<Contains><FieldRef Name=\"Title\"/><Value Type=\"Text\">Item16</Value></Contains>`,\n    QueryOptions: `<QueryOptions>\n    <IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns>\n    <DateInUtc>False</DateInUtc>\n    <IncludePermissions>TRUE</IncludePermissions>\n    <IncludeAttachmentUrls>FALSE</IncludeAttachmentUrls>\n    <Folder>My List</Folder></QueryOptions>`,\n};\n\n// get list items\nconst r = await list.getListItemChangesSinceToken(changeLogItemQuery);\n\n// log resulting XML to console\nconsole.log(r);\n
"},{"location":"sp/lists/#recycle-a-list","title":"Recycle a list","text":"

Removes the list from the web's list collection and puts it in the recycle bin.

await list.recycle();\n
"},{"location":"sp/lists/#render-list-data","title":"Render list data","text":"
import { IRenderListData } from \"@pnp/sp/lists\";\n\n// render list data, top 5 items\nconst r: IRenderListData = await list.renderListData(\"<View><RowLimit>5</RowLimit></View>\");\n\n// log array of items in response\nconsole.log(r.Row);\n
"},{"location":"sp/lists/#render-list-data-as-stream","title":"Render list data as stream","text":"
import { IRenderListDataParameters } from \"@pnp/sp/lists\";\n// setup parameters object\nconst renderListDataParams: IRenderListDataParameters = {\n    ViewXml: \"<View><RowLimit>5</RowLimit></View>\",\n};\n// render list data as stream\nconst r = await list.renderListDataAsStream(renderListDataParams);\n// log array of items in response\nconsole.log(r.Row);\n

You can also supply other options to renderListDataAsStream including override parameters and query params. This can be helpful when looking to apply sorting to the returned data.

import { IRenderListDataParameters } from \"@pnp/sp/lists\";\n// setup parameters object\nconst renderListDataParams: IRenderListDataParameters = {\n    ViewXml: \"<View><RowLimit>5</RowLimit></View>\",\n};\nconst overrideParams = {\n    ViewId = \"{view guid}\"\n};\n// OR if you don't want to supply override params use null\n// overrideParams = null;\n// Set the query params using a map\nconst query = new Map<string, string>();\nquery.set(\"SortField\", \"{AField}\");\nquery.set(\"SortDir\", \"Desc\");\n// render list data as stream\nconst r = await list.renderListDataAsStream(renderListDataParams, overrideParams, query);\n// log array of items in response\nconsole.log(r.Row);\n
"},{"location":"sp/lists/#reserve-list-item-id-for-idempotent-list-item-creation","title":"Reserve list item Id for idempotent list item creation","text":"
const listItemId = await list.reserveListItemId();\n\n// log id to console\nconsole.log(listItemId);\n
"},{"location":"sp/lists/#add-a-list-item-using-path-folder-validation-and-set-field-values","title":"Add a list item using path (folder), validation and set field values","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\nconst list = await sp.webs.lists.getByTitle(\"MyList\").select(\"Title\", \"ParentWebUrl\")();\nconst formValues: IListItemFormUpdateValue[] = [\n                {\n                    FieldName: \"Title\",\n                    FieldValue: title,\n                },\n            ];\n\nlist.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)\n\n
"},{"location":"sp/lists/#content-types-imports","title":"content-types imports","text":""},{"location":"sp/lists/#contenttypes","title":"contentTypes","text":"

Get all content types for a list

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nimport \"@pnp/sp/content-types/list\";\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.contentTypes();\n
"},{"location":"sp/lists/#fields-imports","title":"fields imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";"},{"location":"sp/lists/#fields","title":"fields","text":"

Get all the fields for a list

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nimport \"@pnp/sp/fields/list\";\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.fields();\n

Add a field to the site, then add the site field to a list

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst fld = await sp.site.rootWeb.fields.addText(\"MyField\");\nawait sp.web.lists.getByTitle(\"MyList\").fields.createFieldAsXml(fld.data.SchemaXml);\n
"},{"location":"sp/lists/#folders","title":"folders","text":"

Get the root folder of a list.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/folders/list\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.rootFolder();\n
"},{"location":"sp/lists/#forms","title":"forms","text":"
import \"@pnp/sp/forms/list\";\n\nconst r = await list.forms();\n
"},{"location":"sp/lists/#items","title":"items","text":"

Get a collection of list items.

import \"@pnp/sp/items/list\";\n\nconst r = await list.items();\n
"},{"location":"sp/lists/#views","title":"views","text":"

Get the default view of the list

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views/list\";\n\nconst sp = spfi(...);\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst views = await list.views();\nconst defaultView = await list.defaultView();\n

Get a list view by Id

const view = await list.getView(defaultView.Id).select(\"Title\")();\n
"},{"location":"sp/lists/#security-imports","title":"security imports","text":"

To work with list security, you can import the list methods as follows:

import \"@pnp/sp/security/list\";\n

For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.

"},{"location":"sp/lists/#subscriptions","title":"subscriptions","text":"

Get all subscriptions on the list

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/subscriptions/list\";\n\nconst sp = spfi(...);\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst subscriptions = await list.subscriptions();\n
"},{"location":"sp/lists/#usercustomactions","title":"userCustomActions","text":"

Get a collection of the list's user custom actions.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/user-custom-actions/web\"\n\nconst sp = spfi(...);\nconst list = sp.web.lists.getByTitle(\"Documents\");\nconst r = await list.userCustomActions();\n
"},{"location":"sp/lists/#getparentinfos","title":"getParentInfos","text":"

Gets information about an list, including details about the parent list root folder, and parent web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"Documents\");\nawait list.getParentInfos();\n
"},{"location":"sp/navigation/","title":"@pnp/sp - navigation","text":""},{"location":"sp/navigation/#navigation-service","title":"Navigation Service","text":""},{"location":"sp/navigation/#getmenustate","title":"getMenuState","text":"

The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.

The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\\,containingcomma

NOTE: the , separator can be escaped using the \\ as escape character as done in the example above. The string above would split like:

  • property1
  • property2
  • property3,containingcomma
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n\n// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels.\nconst state = await sp.navigation.getMenuState();\n\n// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5\nconst state2 = await sp.navigation.getMenuState(\"1002\", 5);\n\n// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5\nconst state3 = await sp.navigation.getMenuState(null, 5, \"CurrentNavSiteMapProviderNoEncode\");\n
"},{"location":"sp/navigation/#getmenunodekey","title":"getMenuNodeKey","text":"

Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n\nconst key = await sp.navigation.getMenuNodeKey(\"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\");\n
"},{"location":"sp/navigation/#web-navigation","title":"Web Navigation","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/webs\";import \"@pnp/sp/navigation\";

The navigation object contains two properties \"quicklaunch\" and \"topnavigationbar\". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.

"},{"location":"sp/navigation/#get-navigation","title":"Get navigation","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n\nconst top = await sp.web.navigation.topNavigationBar();\nconst quick = await sp.web.navigation.quicklaunch();\n

For the following examples we will refer to a variable named \"nav\" that is understood to be one of topNavigationBar or quicklaunch:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\n// note we are just getting a ref to the nav object, not executing a request\nconst nav = sp.web.navigation.topNavigationBar;\n// -- OR -- \n// note we are just getting a ref to the nav object, not executing a request\nconst nav = sp.web.navigation.quicklaunch;\n
"},{"location":"sp/navigation/#getbyid","title":"getById","text":"
import \"@pnp/sp/navigation\";\n\nconst node = await nav.getById(3)();\n
"},{"location":"sp/navigation/#add","title":"add","text":"
import \"@pnp/sp/navigation\";\n\nconst result = await nav.add(\"Node Title\", \"/sites/dev/pages/mypage.aspx\", true);\n\nconst nodeDataRaw = result.data;\n\n// request the data from the created node\nconst nodeData = result.node();\n
"},{"location":"sp/navigation/#moveafter","title":"moveAfter","text":"

Places a navigation node after another node in the tree

import \"@pnp/sp/navigation\";\n\nconst node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true);\nconst node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true);\nconst node1 = await node1result.node();\nconst node2 = await node2result.node();\n\nawait nav.moveAfter(node1.Id, node2.Id);\n
"},{"location":"sp/navigation/#delete","title":"Delete","text":"

Deletes a given node

import \"@pnp/sp/navigation\";\n\nconst node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true);\nlet nodes = await nav();\n// check we added a node\nlet index = nodes.findIndex(n => n.Id === node1result.data.Id)\n// index >= 0\n\n// delete a node\nawait nav.getById(node1result.data.Id).delete();\n\nnodes = await nav();\nindex = nodes.findIndex(n => n.Id === node1result.data.Id)\n// index = -1\n
"},{"location":"sp/navigation/#update","title":"Update","text":"

You are able to update various properties of a given node, such as the the Title, Url, IsVisible.

You may update the Audience Targeting value for the node by passing in Microsoft Group IDs in the AudienceIds array. Be aware, Audience Targeting must already be enabled on the navigation.

import \"@pnp/sp/navigation\";\n\n\nawait nav.getById(4).update({\n    Title: \"A new title\",\n    AudienceIds:[\"d50f9511-b811-4d76-b20a-0d6e1c8095f7\"],\n    Url:\"/sites/dev/SitePages/home.aspx\",\n    IsVisible:false\n});\n
"},{"location":"sp/navigation/#children","title":"Children","text":"

The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch.

import \"@pnp/sp/navigation\";\n\nconst childrenData = await nav.getById(1).children();\n\n// add a child\nawait nav.getById(1).children.add(\"Title\", \"Url\", true);\n
"},{"location":"sp/permissions/","title":"@pnp/sp - permissions","text":"

A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.

Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.

"},{"location":"sp/permissions/#get-role-assignments","title":"Get Role Assignments","text":"

This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst roles = await sp.web.roleAssignments();\n
"},{"location":"sp/permissions/#first-unique-ancestor-securable-object","title":"First Unique Ancestor Securable Object","text":"

This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst obj = await sp.web.firstUniqueAncestorSecurableObject();\n
"},{"location":"sp/permissions/#user-effective-permissions","title":"User Effective Permissions","text":"

This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst perms = await sp.web.getUserEffectivePermissions(\"i:0#.f|membership|user@site.com\");\n\nconst perms2 = await sp.web.getCurrentUserEffectivePermissions();\n
"},{"location":"sp/permissions/#user-has-permissions","title":"User Has Permissions","text":"

Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst perms = await sp.web.userHasPermissions(\"i:0#.f|membership|user@site.com\", PermissionKind.ApproveItems);\n\nconst perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems);\n
"},{"location":"sp/permissions/#has-permissions","title":"Has Permissions","text":"

If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\nconst perms = await sp.web.getCurrentUserEffectivePermissions();\nif (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) {\n    // ...\n}\n
"},{"location":"sp/profiles/","title":"@pnp/sp/profiles","text":"

The profile services allows you to work with the SharePoint User Profile Store.

"},{"location":"sp/profiles/#profiles","title":"Profiles","text":"

Profiles is accessed directly from the root sp object.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/profiles\";\n
"},{"location":"sp/profiles/#get-edit-profile-link-for-the-current-user","title":"Get edit profile link for the current user","text":"
getEditProfileLink(): Promise<string>\n
const sp = spfi(...);\nconst editProfileLink = await sp.profiles.getEditProfileLink();\n
"},{"location":"sp/profiles/#is-my-people-list-public","title":"Is My People List Public","text":"

Provides a boolean that indicates if the current users \"People I'm Following\" list is public or not

getIsMyPeopleListPublic(): Promise<boolean>\n
const sp = spfi(...);\nconst isPublic = await sp.profiles.getIsMyPeopleListPublic();\n
"},{"location":"sp/profiles/#find-out-if-the-current-user-is-followed-by-another-user","title":"Find out if the current user is followed by another user","text":"

Provides a boolean that indicates if the current users is followed by a specific user.

amIFollowedBy(loginName: string): Promise<boolean>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst isFollowedBy = await sp.profiles.amIFollowedBy(loginName);\n
"},{"location":"sp/profiles/#find-out-if-i-am-following-a-specific-user","title":"Find out if I am following a specific user","text":"

Provides a boolean that indicates if the current users is followed by a specific user.

amIFollowing(loginName: string): Promise<boolean>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst following = await sp.profiles.amIFollowing(loginName);\n
"},{"location":"sp/profiles/#get-the-tags-i-follow","title":"Get the tags I follow","text":"

Gets the tags the current user is following. Accepts max count, default is 20.

getFollowedTags(maxCount = 20): Promise<string[]>\n
const sp = spfi(...);\nconst tags = await sp.profiles.getFollowedTags();\n
"},{"location":"sp/profiles/#get-followers-for-a-specific-user","title":"Get followers for a specific user","text":"

Gets the people who are following the specified user.

getFollowersFor(loginName: string): Promise<any[]>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst followers = await sp.profiles.getFollowersFor(loginName);\nfollowers.forEach((value) => {\n  ...\n});\n
"},{"location":"sp/profiles/#get-followers-for-the-current","title":"Get followers for the current","text":"

Gets the people who are following the current user.

myFollowers(): ISPCollection\n
const sp = spfi(...);\nconst folowers = await sp.profiles.myFollowers();\n
"},{"location":"sp/profiles/#get-the-properties-for-the-current-user","title":"Get the properties for the current user","text":"

Gets user properties for the current user.

myProperties(): ISPInstance\n
const sp = spfi(...);\nconst profile = await sp.profiles.myProperties();\nconsole.log(profile.DisplayName);\nconsole.log(profile.Email);\nconsole.log(profile.Title);\nconsole.log(profile.UserProfileProperties.length);\n\n// Properties are stored in Key/Value pairs,\n// so parse into an object called userProperties\nvar props = {};\nprofile.UserProfileProperties.forEach((prop) => {\n  props[prop.Key] = prop.Value;\n});\nprofile.userProperties = props;\nconsole.log(\"Account Name: \" + profile.userProperties.AccountName);\n
// you can also select properties to return before\nconst sp = spfi(...);\nconst profile = await sp.profiles.myProperties.select(\"Title\", \"Email\")();\nconsole.log(profile.Email);\nconsole.log(profile.Title);\n
"},{"location":"sp/profiles/#gets-people-specified-user-is-following","title":"Gets people specified user is following","text":"
getPeopleFollowedBy(loginName: string): Promise<any[]>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst folowers = await sp.profiles.getFollowersFor(loginName);\nfollowers.forEach((value) => {\n  ...\n});\n
"},{"location":"sp/profiles/#gets-properties-for-a-specified-user","title":"Gets properties for a specified user","text":"
getPropertiesFor(loginName: string): Promise<any>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst profile = await sp.profiles.getPropertiesFor(loginName);\nconsole.log(profile.DisplayName);\nconsole.log(profile.Email);\nconsole.log(profile.Title);\nconsole.log(profile.UserProfileProperties.length);\n\n// Properties are stored in inconvenient Key/Value pairs,\n// so parse into an object called userProperties\nvar props = {};\nprofile.UserProfileProperties.forEach((prop) => {\n  props[prop.Key] = prop.Value;\n});\n\nprofile.userProperties = props;\nconsole.log(\"Account Name: \" + profile.userProperties.AccountName);\n
"},{"location":"sp/profiles/#gets-most-popular-tags","title":"Gets most popular tags","text":"

Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first

trendingTags(): Promise<IHashTagCollection>\n
const sp = spfi(...);\nconst tags = await sp.profiles.trendingTags();\ntags.Items.forEach((tag) => {\n  ...\n});\n
"},{"location":"sp/profiles/#gets-specified-user-profile-property-for-the-specified-user","title":"Gets specified user profile property for the specified user","text":"
getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst propertyName = \"AccountName\";\nconst property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);\n
"},{"location":"sp/profiles/#hide-specific-user-from-list-of-suggested-people","title":"Hide specific user from list of suggested people","text":"

Removes the specified user from the user's list of suggested people to follow.

hideSuggestion(loginName: string): Promise<void>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nawait sp.profiles.hideSuggestion(loginName);\n
"},{"location":"sp/profiles/#is-one-user-following-another","title":"Is one user following another","text":"

Indicates whether the first user is following the second user. First parameter is the account name of the user who might be following the followee. Second parameter is the account name of the user who might be followed by the follower.

isFollowing(follower: string, followee: string): Promise<boolean>\n
const sp = spfi(...);\nconst follower = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst followee = \"i:0#.f|membership|testuser2@mytenant.onmicrosoft.com\";\nconst isFollowing = await sp.profiles.isFollowing(follower, followee);\n
"},{"location":"sp/profiles/#set-user-profile-picture","title":"Set User Profile Picture","text":"

Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB.

setMyProfilePic(profilePicSource: Blob): Promise<void>\n
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/profiles\";\nimport \"@pnp/sp/folders\";\nimport \"@pnp/sp/files\";\n\nconst sp = spfi(...);\n\n// get the blob object through a request or from a file input\nconst blob = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.getByName(\"profile.jpg\").getBlob();\n\nawait sp.profiles.setMyProfilePic(blob);\n
"},{"location":"sp/profiles/#sets-single-value-user-profile-property","title":"Sets single value User Profile property","text":"

accountName The account name of the user propertyName Property name propertyValue Property value

setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise<void>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nawait sp.profiles.setSingleValueProfileProperty(loginName, \"CellPhone\", \"(123) 555-1212\");\n
"},{"location":"sp/profiles/#sets-a-mult-value-user-profile-property","title":"Sets a mult-value User Profile property","text":"

accountName The account name of the user propertyName Property name propertyValues Property values

setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>\n
const sp = spfi(...);\nconst loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\";\nconst propertyName = \"SPS-Skills\";\nconst propertyValues = [\"SharePoint\", \"Office 365\", \"Architecture\", \"Azure\"];\nawait sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues);\nconst profile = await sp.profiles.getPropertiesFor(loginName);\nvar props = {};\nprofile.UserProfileProperties.forEach((prop) => {\n  props[prop.Key] = prop.Value;\n});\nprofile.userProperties = props;\nconsole.log(profile.userProperties[propertyName]);\n
"},{"location":"sp/profiles/#create-personal-site-for-specified-users","title":"Create Personal Site for specified users","text":"

Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Emails The email addresses of the users to provision sites for

createPersonalSiteEnqueueBulk(...emails: string[]): Promise<void>\n
const sp = spfi(...);\nlet userEmails: string[] = [\"testuser1@mytenant.onmicrosoft.com\", \"testuser2@mytenant.onmicrosoft.com\"];\nawait sp.profiles.createPersonalSiteEnqueueBulk(userEmails);\n
"},{"location":"sp/profiles/#get-the-user-profile-of-the-owner-for-the-current-site","title":"Get the user profile of the owner for the current site","text":"
ownerUserProfile(): Promise<IUserProfile>\n
const sp = spfi(...);\nconst profile = await sp.profiles.ownerUserProfile();\n
"},{"location":"sp/profiles/#get-the-user-profile-of-the-current-user","title":"Get the user profile of the current user","text":"
userProfile(): Promise<any>\n
const sp = spfi(...);\nconst profile = await sp.profiles.userProfile();\n
"},{"location":"sp/profiles/#create-personal-site-for-current-user","title":"Create personal site for current user","text":"
createPersonalSite(interactiveRequest = false): Promise<void>\n
const sp = spfi(...);\nawait sp.profiles.createPersonalSite();\n
"},{"location":"sp/profiles/#make-all-profile-data-public-or-private","title":"Make all profile data public or private","text":"

Set the privacy settings for all social data.

shareAllSocialData(share: boolean): Promise<void>\n
const sp = spfi(...);\nawait sp.profiles.shareAllSocialData(true);\n
"},{"location":"sp/profiles/#resolve-a-user-or-group","title":"Resolve a user or group","text":"

Resolves user or group using specified query parameters

clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity>\n
const sp = spfi(...);\nconst result = await sp.profiles.clientPeoplePickerResolveUser({\n  AllowEmailAddresses: true,\n  AllowMultipleEntities: false,\n  MaximumEntitySuggestions: 25,\n  QueryString: 'mbowen@contoso.com'\n});\n
"},{"location":"sp/profiles/#search-a-user-or-group","title":"Search a user or group","text":"

Searches for users or groups using specified query parameters

clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity[]>\n
const sp = spfi(...);\nconst result = await sp.profiles.clientPeoplePickerSearchUser({\n  AllowEmailAddresses: true,\n  AllowMultipleEntities: false,\n  MaximumEntitySuggestions: 25,\n  QueryString: 'John'\n});\n
"},{"location":"sp/publishing-sitepageservice/","title":"@pnp/sp/publishing-sitepageservice","text":"

Through the REST api you are able to call a SP.Publishing.SitePageService method GetCurrentUserMemberships. This method allows you to fetch identifiers of unified groups to which current user belongs. It's an alternative for using graph.me.transitiveMemberOf() method from graph package. Note, method only works with the context of a logged in user, and not with app-only permissions.

"},{"location":"sp/publishing-sitepageservice/#get-current-users-group-memberships","title":"Get current user's group memberships","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/publishing-sitepageservice\";\n\nconst sp = spfi(...);\n\nconst groupIdentifiers = await sp.publishingSitePageService.getCurrentUserMemberships();\n
"},{"location":"sp/recycle-bin/","title":"@pnp/sp/recycle-bin","text":"

The contents of the recycle bin.

"},{"location":"sp/recycle-bin/#irecyclebin-irecyclebinitem","title":"IRecycleBin, IRecycleBinItem","text":""},{"location":"sp/recycle-bin/#work-with-the-contents-of-the-webs-recycle-bin","title":"Work with the contents of the web's Recycle Bin","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/recycle-bin\";\n\nconst sp = spfi(...);\n\n// gets contents of the web's recycle bin\nconst bin = await sp.web.recycleBin();\n\n// gets a specific item from the recycle bin\nconst rbItem = await sp.web.recycleBin.getById(bin[0].id);\n\n// delete the item from the recycle bin\nawait rbItem.delete();\n\n// restore the item from the recycle bin\nawait rbItem.restore();\n\n// move the item to the second-stage (site) recycle bin.\nawait rbItem.moveToSecondStage();\n\n// deletes everything in the recycle bin\nawait sp.web.recycleBin.deleteAll();\n\n// restores everything in the recycle bin\nawait sp.web.recycleBin.restoreAll();\n\n// moves contents of recycle bin to second-stage (site) recycle bin.\nawait sp.web.recycleBin.moveAllToSecondStage();\n\n// deletes contents of the second-stage (site) recycle bin.\nawait sp.web.recycleBin.deleteAllSecondStageItems();\n
"},{"location":"sp/recycle-bin/#work-with-the-contents-of-the-second-stage-site-recycle-bin","title":"Work with the contents of the Second-stage (site) Recycle Bin","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport \"@pnp/sp/recycle-bin\";\n\nconst sp = spfi(...);\n\n// gets contents of the second-stage recycle bin\nconst ssBin = await sp.site.recycleBin();\n\n// gets a specific item from the second-stage recycle bin\nconst rbItem = await sp.site.recycleBin.getById(ssBin[0].id);\n\n// delete the item from the second-stage recycle bin\nawait rbItem.delete();\n\n// restore the item from the second-stage recycle bin\nawait rbItem.restore();\n\n// deletes everything in the second-stage recycle bin\nawait sp.site.recycleBin.deleteAll();\n\n// restores everything in the second-stage recycle bin\nawait sp.site.recycleBin.restoreAll();\n
"},{"location":"sp/regional-settings/","title":"@pnp/sp/regional-settings","text":"

The regional settings module helps with managing dates and times across various timezones.

"},{"location":"sp/regional-settings/#iregionalsettings","title":"IRegionalSettings","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings/web\";\n\nconst sp = spfi(...);\n\n// get all the web's regional settings\nconst s = await sp.web.regionalSettings();\n\n// select only some settings to return\nconst s2 = await sp.web.regionalSettings.select(\"DecimalSeparator\", \"ListSeparator\", \"IsUIRightToLeft\")();\n
"},{"location":"sp/regional-settings/#installed-languages","title":"Installed Languages","text":"

You can get a list of the installed languages in the web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings/web\";\n\nconst sp = spfi(...);\n\nconst s = await sp.web.regionalSettings.getInstalledLanguages();\n

The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.

"},{"location":"sp/regional-settings/#timezones","title":"TimeZones","text":"

You can also get information about the selected timezone in the web and all of the defined timezones.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings/web\";\n\nconst sp = spfi(...);\n\n// get the web's configured timezone\nconst s = await sp.web.regionalSettings.timeZone();\n\n// select just the Description and Id\nconst s2 = await sp.web.regionalSettings.timeZone.select(\"Description\", \"Id\")();\n\n// get all the timezones\nconst s3 = await sp.web.regionalSettings.timeZones();\n\n// get a specific timezone by id\n// list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx\nconst s4 = await sp.web.regionalSettings.timeZones.getById(23);\nconst s5 = await s.localTimeToUTC(new Date());\n\n// convert a given date from web's local time to UTC time\nconst s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date());\n\n// convert a given date from UTC time to web's local time\nconst s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date())\nconst s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))\n
"},{"location":"sp/regional-settings/#title-and-description-resources","title":"Title and Description Resources","text":"

Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/regional-settings\";\n\nconst sp = spfi(...);\n\n//\n// The below methods appears on\n// - Web\n// - List\n// - Field\n// - ContentType\n// - User Custom Action\n//\n// after you import @pnp/sp/regional-settings\n//\n// you can also import just parts of the regional settings:\n// import \"@pnp/sp/regional-settings/web\";\n// import \"@pnp/sp/regional-settings/list\";\n// import \"@pnp/sp/regional-settings/content-type\";\n// import \"@pnp/sp/regional-settings/field\";\n// import \"@pnp/sp/regional-settings/user-custom-actions\";\n\n\nconst title = await sp.web.titleResource(\"en-us\");\nconst title2 = await sp.web.titleResource(\"de-de\");\n\nconst description = await sp.web.descriptionResource(\"en-us\");\nconst description2 = await sp.web.descriptionResource(\"de-de\");\n

You can only read the values through the REST API, not set the value.

"},{"location":"sp/related-items/","title":"@pnp/sp/related-items","text":"

The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.

"},{"location":"sp/related-items/#setup","title":"Setup","text":"

Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work.

import { spfi, SPFx, extractWebUrl } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/related-items/web\";\nimport \"@pnp/sp/lists/web\";\nimport \"@pnp/sp/items/list\";\nimport \"@pnp/sp/files/list\";\nimport { IList } from \"@pnp/sp/lists\";\nimport { getRandomString } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\n// setup some lists (or just use existing ones this is just to show the complete process)\n// we need two lists to use for creating related items, they need to use template 107 (task list)\nconst ler1 = await sp.web.lists.ensure(\"RelatedItemsSourceList\", \"\", 107);\nconst ler2 = await sp.web.lists.ensure(\"RelatedItemsTargetList\", \"\", 107);\n\nconst sourceList = ler1.list;\nconst targetList = ler2.list;\n\nconst sourceListName = await sourceList.select(\"Id\")().then(r => r.Id);\nconst targetListName = await targetList.select(\"Id\")().then(r => r.Id);\n\n// or whatever you need to get the web url, both our example lists are in the same web.\nconst webUrl = sp.web.toUrl();\n\n// ...individual samples start here\n
"},{"location":"sp/related-items/#addsinglelink","title":"addSingleLink","text":"
const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n
"},{"location":"sp/related-items/#addsinglelinktourl","title":"addSingleLinkToUrl","text":"

This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document.

// get a file's server relative url in some manner, here we add one\nconst file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, \"Content\", true).then(r => r.data);\n// add an item or get an item from the task list\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\nawait sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);\n
"},{"location":"sp/related-items/#addsinglelinkfromurl","title":"addSingleLinkFromUrl","text":"

This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.

"},{"location":"sp/related-items/#deletesinglelink","title":"deleteSingleLink","text":"

This method allows you to delete a link previously created.

const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add the link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n\n// delete the link\nawait sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n
"},{"location":"sp/related-items/#getrelateditems","title":"getRelatedItems","text":"

Gets the related items for an item

import { IRelatedItem } from \"@pnp/sp/related-items\";\n\nconst sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n\nconst targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);\n\nconst items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id);\n\n// items.length === 2\n

Related items are defined by the IRelatedItem interface

export interface IRelatedItem {\n    ListId: string;\n    ItemId: number;\n    Url: string;\n    Title: string;\n    WebId: string;\n    IconUrl: string;\n}\n
"},{"location":"sp/related-items/#getpageonerelateditems","title":"getPageOneRelatedItems","text":"

Gets an abbreviated set of related items

import { IRelatedItem } from \"@pnp/sp/related-items\";\n\nconst sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\nconst targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);\n\nconst targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);\n\n// add a link\nawait sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);\n\nconst items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id);\n\n// items.length === 2\n
"},{"location":"sp/search/","title":"@pnp/sp/search","text":"

Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.

"},{"location":"sp/search/#search","title":"Search","text":"

Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\n// text search using SharePoint default values for other parameters\nconst results: SearchResults = await sp.search(\"test\");\n\nconsole.log(results.ElapsedTime);\nconsole.log(results.RowCount);\nconsole.log(results.PrimarySearchResults);\n\n\n// define a search query object matching the ISearchQuery interface\nconst results2: SearchResults = await sp.search(<ISearchQuery>{\n    Querytext: \"test\",\n    RowLimit: 10,\n    EnableInterleaving: true,\n});\n\nconsole.log(results2.ElapsedTime);\nconsole.log(results2.RowCount);\nconsole.log(results2.PrimarySearchResults);\n\n// define a query using a builder\nconst builder = SearchQueryBuilder(\"test\").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites;\nconst results3 = await sp.search(builder);\n\nconsole.log(results3.ElapsedTime);\nconsole.log(results3.RowCount);\nconsole.log(results3.PrimarySearchResults);\n
"},{"location":"sp/search/#search-result-caching","title":"Search Result Caching","text":"

Starting with v3 you can use any of the caching behaviors with search and the results will be cached. Please see here for more details on caching options.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\";\nimport { Caching } from \"@pnp/queryable\";\n\nconst sp = spfi(...).using(Caching());\n\nsp.search({/* ... query */}).then((r: SearchResults) => {\n\n    console.log(r.ElapsedTime);\n    console.log(r.RowCount);\n    console.log(r.PrimarySearchResults);\n});\n\n// use a query builder\nconst builder = SearchQueryBuilder(\"test\").rowLimit(3);\n\n// supply a search query builder and caching options\nconst results2 = await sp.search(builder);\n\nconsole.log(results2.TotalRows);\n
"},{"location":"sp/search/#paging-with-searchresultsgetpage","title":"Paging with SearchResults.getPage","text":"

Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\n// this will hold our current results\nlet currentResults: SearchResults = null;\nlet page = 1;\n\n// triggered on page load or through some other means\nfunction onStart() {\n\n    // construct our query that will be used throughout the paging process, likely from user input\n    const q = SearchQueryBuilder(\"test\").rowLimit(5);\n    const results = await sp.search(q);\n    currentResults = results; // set the current results\n    page = 1; // reset page counter\n    // update UI...\n}\n\n// triggered by an event\nasync function next() {\n\n    currentResults = await currentResults.getPage(++page);\n    // update UI...\n}\n\n// triggered by an event\nasync function prev() {\n\n    currentResults = await currentResults.getPage(--page);\n    // update UI...\n}\n
"},{"location":"sp/search/#searchquerybuilder","title":"SearchQueryBuilder","text":"

The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { SearchQueryBuilder, SearchResults, ISearchQuery } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\n// basic usage\nlet q = SearchQueryBuilder().text(\"test\").rowLimit(4).enablePhonetic;\n\nsp.search(q).then(h => { /* ... */ });\n\n// provide a default query text at creation\nlet q2 = SearchQueryBuilder(\"text\").rowLimit(4).enablePhonetic;\n\nconst results: SearchResults = await sp.search(q2);\n\n// provide query text and a template for\n// shared settings across queries that can\n// be overwritten by individual builders\nconst appSearchSettings: ISearchQuery = {\n    EnablePhonetic: true,\n    HiddenConstraints: \"reports\"\n};\n\nlet q3 = SearchQueryBuilder(\"test\", appSearchSettings).enableQueryRules;\nlet q4 = SearchQueryBuilder(\"financial data\", appSearchSettings).enableSorting.enableStemming;\nconst results2 = await sp.search(q3);\nconst results3 = sp.search(q4);\n
"},{"location":"sp/search/#search-suggest","title":"Search Suggest","text":"

Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/search\";\nimport { ISuggestQuery, ISuggestResult } from \"@pnp/sp/search\";\n\nconst sp = spfi(...);\n\nconst results = await sp.searchSuggest(\"test\");\n\nconst results2 = await sp.searchSuggest({\n    querytext: \"test\",\n    count: 5,\n} as ISuggestQuery);\n
"},{"location":"sp/search/#search-factory","title":"Search Factory","text":"

You can also configure a search or suggest query against any valid SP url using the factory methods.

In this case you'll need to ensure you add observers, or use the tuple constructor to inherit

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/web\";\nimport \"@pnp/sp/search\";\nimport { Search, Suggest } from \"@pnp/sp/search\";\nimport { SPDefault } from \"@pnp/nodejs\";\n\nconst sp = spfi(...);\n\n// set the url for search\nconst searcher = Search([sp.web, \"https://mytenant.sharepoint.com/sites/dev\"]);\n\n// this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder)\nconst results = await searcher(\"test\");\n\n// you can reuse the ISearch instance\nconst results2 = await searcher(\"another query\");\n\n// same process works for Suggest\nconst suggester = Suggest([sp.web, \"https://mytenant.sharepoint.com/sites/dev\"]);\n\nconst suggestions = await suggester({ querytext: \"test\" });\n\n// resetting the observers on the instance\nconst searcher2 = Search(\"https://mytenant.sharepoint.com/sites/dev\").using(SPDefault({\n  msal: {\n    config: {...},\n    scopes: [...],\n  },\n}));\n\nconst results3 = await searcher2(\"test\");\n
"},{"location":"sp/security/","title":"@pnp/sp/security","text":"

There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below.

Site permissions are managed on the root web of the site collection.

"},{"location":"sp/security/#a-note-on-selective-imports-for-security","title":"A Note on Selective Imports for Security","text":"

Because the method are shared you can opt to import only the methods for one of the instances.

import \"@pnp/sp/security/web\";\nimport \"@pnp/sp/security/list\";\nimport \"@pnp/sp/security/item\";\n

Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module:

import \"@pnp/sp/security\";\n
"},{"location":"sp/security/#securable-methods","title":"Securable Methods","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/security/list\";\nimport \"@pnp/sp/site-users/web\";\nimport { IList } from \"@pnp/sp/lists\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\n// ensure we have a list\nconst ler = await sp.web.lists.ensure(\"SecurityTestingList\");\nconst list: IList = ler.list;\n\n// role assignments (see section below)\nawait list.roleAssignments();\n\n// data will represent one of the possible parents Site, Web, or List\nconst data = await list.firstUniqueAncestorSecurableObject();\n\n// getUserEffectivePermissions\nconst users = await sp.web.siteUsers.top(1).select(\"LoginName\")();\nconst perms = await list.getUserEffectivePermissions(users[0].LoginName);\n\n// getCurrentUserEffectivePermissions\nconst perms2 = list.getCurrentUserEffectivePermissions();\n\n// userHasPermissions\nconst v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems)\n\n// currentUserHasPermissions\nconst v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems)\n\n// breakRoleInheritance\nawait list.breakRoleInheritance();\n// copy existing permissions\nawait list.breakRoleInheritance(true);\n// copy existing permissions and reset all child securables to the new permissions\nawait list.breakRoleInheritance(true, true);\n\n// resetRoleInheritance\nawait list.resetRoleInheritance();\n
"},{"location":"sp/security/#web-specific-methods","title":"Web Specific methods","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/security/web\";\n\nconst sp = spfi(...);\n\n// role definitions (see section below)\nconst defs = await sp.web.roleDefinitions();\n
"},{"location":"sp/security/#role-assignments","title":"Role Assignments","text":"

Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/security/web\";\nimport \"@pnp/sp/site-users/web\";\nimport { IList } from \"@pnp/sp/lists\";\nimport { PermissionKind } from \"@pnp/sp/security\";\n\nconst sp = spfi(...);\n\n// ensure we have a list\nconst ler = await sp.web.lists.ensure(\"SecurityTestingList\");\nconst list: IList = ler.list;\n\n// list role assignments\nconst assignments = await list.roleAssignments();\n\n// add a role assignment\nconst defs = await sp.web.roleDefinitions();\nconst user = await sp.web.currentUser();\nconst r = await list.roleAssignments.add(user.Id, defs[0].Id);\n\n// remove a role assignment\nconst { Id: fullRoleDefId } = await sp.web.roleDefinitions.getByName('Full Control')();\nconst ras = await list.roleAssignments();\n// filter/find the role assignment you want to remove\n// here we just grab the first\nconst ra = ras.find(v => true);\nconst r = await list.roleAssignments.remove(ra.PrincipalId, fullRoleDefId);\n\n// read role assignment info\nconst info = await list.roleAssignments.getById(ra.Id)();\n\n// get the groups\nconst info2 = await list.roleAssignments.getById(ra.Id).groups();\n\n// get the bindings\nconst info3 = await list.roleAssignments.getById(ra.Id).bindings();\n\n// delete a role assignment (same as remove)\nconst ras = await list.roleAssignments();\n// filter/find the role assignment you want to remove\n// here we just grab the first\nconst ra = ras.find(v => true);\n\n// delete it\nawait list.roleAssignments.getById(ra.Id).delete();\n
"},{"location":"sp/security/#role-definitions","title":"Role Definitions","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/security/web\";\n\nconst sp = spfi(...);\n\n// read role definitions\nconst defs = await sp.web.roleDefinitions();\n\n// get by id\nconst def = await sp.web.roleDefinitions.getById(5)();\nconst def = await sp.web.roleDefinitions.getById(5).select(\"Name\", \"Order\")();\n\n// get by name\nconst def = await sp.web.roleDefinitions.getByName(\"Full Control\")();\nconst def = await sp.web.roleDefinitions.getByName(\"Full Control\").select(\"Name\", \"Order\")();\n\n// get by type\nconst def = await sp.web.roleDefinitions.getByType(5)();\nconst def = await sp.web.roleDefinitions.getByType(5).select(\"Name\", \"Order\")();\n\n// add\n// name The new role definition's name\n// description The new role definition's description\n// order The order in which the role definition appears\n// basePermissions The permissions mask for this role definition\nconst rdar = await sp.web.roleDefinitions.add(\"title\", \"description\", 99, { High: 1, Low: 2 });\n\n\n\n// the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example\n\n// delete\nawait sp.web.roleDefinitions.getById(5).delete();\n\n// update\nconst res = sp.web.roleDefinitions.getById(5).update({ Name: \"New Name\" });\n
"},{"location":"sp/security/#get-list-items-with-unique-permissions","title":"Get List Items with Unique Permissions","text":"

In order to get a list of items that have unique permissions you have to specifically select the '' field and then filter on the client.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\nimport \"@pnp/sp/security/items\";\n\nconst sp = spfi(...);\n\nconst listItems = await sp.web.lists.getByTitle(\"pnplist\").items.select(\"Id, HasUniqueRoleAssignments\")();\n\n//Loop over list items filtering for HasUniqueRoleAssignments value\n\n
"},{"location":"sp/sharing/","title":"@pnp/sp/sharing","text":"

Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.

One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.

"},{"location":"sp/sharing/#imports","title":"Imports","text":"

In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.

"},{"location":"sp/sharing/#import-all","title":"Import All","text":"

To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module:

import \"@pnp/sp/sharing\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.siteUsers.getByEmail(\"user@site.com\")();\nconst r = await sp.web.shareWith(user.LoginName);\n
"},{"location":"sp/sharing/#selective-import","title":"Selective Import","text":"

Import only the web's sharing methods into the library

import \"@pnp/sp/sharing/web\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { spfi } from \"@pnp/sp\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.siteUsers.getByEmail(\"user@site.com\")();\nconst r = await sp.web.shareWith(user.LoginName);\n
"},{"location":"sp/sharing/#getsharelink","title":"getShareLink","text":"

Applies to: Item, Folder, File

Creates a sharing link for the given resource with an optional expiration.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport { SharingLinkKind, IShareLinkResponse } from \"@pnp/sp/sharing\";\nimport { dateAdd } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView);\n\nconsole.log(JSON.stringify(result, null, 2));\n\n\nconst result2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), \"day\", 5));\n\nconsole.log(JSON.stringify(result2, null, 2));\n
"},{"location":"sp/sharing/#sharewith","title":"shareWith","text":"

Applies to: Item, Folder, File, Web

Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders/web\";\nimport \"@pnp/sp/files/web\";\nimport { ISharingResult, SharingRole } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\");\n\nconsole.log(JSON.stringify(result, null, 2));\n\n// Share and allow editing\nconst result2 = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit);\n\nconsole.log(JSON.stringify(result2, null, 2));\n\n\n// share folder\nconst result3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").shareWith(\"i:0#.f|membership|user@site.com\");\n\n// Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children)\nawait sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit, true, true);\n\n// Share a file\nawait sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\");\n\n// Share a file with edit permissions\nawait sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit);\n
"},{"location":"sp/sharing/#shareobject-shareobjectraw","title":"shareObject & shareObjectRaw","text":"

Applies to: Web

Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport { ISharingResult, SharingRole } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// Share an object in this web\nconst result = await sp.web.shareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", \"i:0#.f|membership|user@site.com\", SharingRole.View);\n\n// Share an object with all settings available\nawait sp.web.shareObjectRaw({\n    url: \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\",\n    peoplePickerInput: [{ Key: \"i:0#.f|membership|user@site.com\" }],\n    roleValue: \"role: 1973741327\",\n    groupId: 0,\n    propagateAcl: false,\n    sendEmail: true,\n    includeAnonymousLinkInEmail: false,\n    emailSubject: \"subject\",\n    emailBody: \"body\",\n    useSimplifiedRoles: true,\n});\n
"},{"location":"sp/sharing/#unshareobject","title":"unshareObject","text":"

Applies to: Web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport { ISharingResult } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.unshareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\");\n
"},{"location":"sp/sharing/#checksharingpermissions","title":"checkSharingPermissions","text":"

Applies to: Item, Folder, File

Checks Permissions on the list of Users and returns back role the users have on the Item.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing/folders\";\nimport \"@pnp/sp/folders/web\";\nimport { SharingEntityPermission } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// check the sharing permissions for a folder\nconst perms = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").checkSharingPermissions([{ alias: \"i:0#.f|membership|user@site.com\" }]);\n
"},{"location":"sp/sharing/#getsharinginformation","title":"getSharingInformation","text":"

Applies to: Item, Folder, File

Get Sharing Information.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { ISharingInformation } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// Get the sharing information for a folder\nconst info = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation();\n\n// get sharing informaiton with a request object\nconst info2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation({\n    maxPrincipalsToReturn: 10,\n    populateInheritedLinks: true,\n});\n\n// get sharing informaiton using select and expand, NOTE expand comes first in the API signature\nconst info3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation({}, [\"permissionsInformation\"], [\"permissionsInformation\",\"anyoneLinkTrackUsers\"]);\n
"},{"location":"sp/sharing/#getobjectsharingsettings","title":"getObjectSharingSettings","text":"

Applies to: Item, Folder, File

Gets the sharing settings

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { IObjectSharingSettings } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\n// Gets the sharing object settings\nconst settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getObjectSharingSettings();\n
"},{"location":"sp/sharing/#unshare","title":"unshare","text":"

Applies to: Item, Folder, File

Unshares a given resource

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { ISharingResult } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshare();\n
"},{"location":"sp/sharing/#deletesharinglinkbykind","title":"deleteSharingLinkByKind","text":"

Applies to: Item, Folder, File

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { ISharingResult, SharingLinkKind } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nconst result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);\n
"},{"location":"sp/sharing/#unsharelink","title":"unshareLink","text":"

Applies to: Item, Folder, File

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/sharing\";\nimport \"@pnp/sp/folders\";\nimport { SharingLinkKind } from \"@pnp/sp/sharing\";\n\nconst sp = spfi(...);\n\nawait sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit);\n\n// specify the sharing link id if available\nawait sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit, \"12345\");\n
"},{"location":"sp/site-designs/","title":"@pnp/sp/site-designs","text":"

You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information.

"},{"location":"sp/site-designs/#site-designs","title":"Site Designs","text":""},{"location":"sp/site-designs/#create-a-new-site-design","title":"Create a new site design","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// WebTemplate: 64 Team site template, 68 Communication site template\nconst siteDesign = await sp.siteDesigns.createSiteDesign({\n    SiteScriptIds: [\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"],\n    Title: \"SiteDesign001\",\n    WebTemplate: \"64\",\n});\n\nconsole.log(siteDesign.Title);\n
"},{"location":"sp/site-designs/#applying-a-site-design-to-a-site","title":"Applying a site design to a site","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Limited to 30 actions in a site script, but runs synchronously\nawait sp.siteDesigns.applySiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\",\"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\");\n\n// Better use the following method for 300 actions in a site script\nconst task = await sp.web.addSiteDesignTask(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\n
"},{"location":"sp/site-designs/#retrieval","title":"Retrieval","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Retrieving all site designs\nconst allSiteDesigns = await sp.siteDesigns.getSiteDesigns();\nconsole.log(`Total site designs: ${allSiteDesigns.length}`);\n\n// Retrieving a single site design by Id\nconst siteDesign = await sp.siteDesigns.getSiteDesignMetadata(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nconsole.log(siteDesign.Title);\n
"},{"location":"sp/site-designs/#update-and-delete","title":"Update and delete","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Update\nconst updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", Title: \"SiteDesignUpdatedTitle001\" });\n\n// Delete\nawait sp.siteDesigns.deleteSiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\n
"},{"location":"sp/site-designs/#setting-rightspermissions","title":"Setting Rights/Permissions","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\n// Get\nconst rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nconsole.log(rights.length > 0 ? rights[0].PrincipalName : \"\");\n\n// Grant\nawait sp.siteDesigns.grantSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]);\n\n// Revoke\nawait sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]);\n\n// Reset all view rights\nconst rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nawait sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", rights.map(u => u.PrincipalName));\n
"},{"location":"sp/site-designs/#get-a-history-of-site-designs-that-have-run-on-a-web","title":"Get a history of site designs that have run on a web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-designs\";\n\nconst sp = spfi(...);\n\nconst runs = await sp.web.getSiteDesignRuns();\nconst runs2 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\");\n\n// Get runs specific to a site design\nconst runs3 = await sp.web.getSiteDesignRuns(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\nconst runs4 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\", \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");\n\n// For more information about the site script actions\nconst runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID);\nconst runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus(\"https://TENANT.sharepoint.com/sites/mysite\", runs[0].ID);\n\n
"},{"location":"sp/site-groups/","title":"@pnp/sp/site-groups","text":"

The site groups module provides methods to manage groups for a sharepoint site.

"},{"location":"sp/site-groups/#isitegroups","title":"ISiteGroups","text":""},{"location":"sp/site-groups/#get-all-site-groups","title":"Get all site groups","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// gets all site groups of the web\nconst groups = await sp.web.siteGroups();\n
"},{"location":"sp/site-groups/#get-the-associated-groups-of-a-web","title":"Get the associated groups of a web","text":"

You can get the associated Owner, Member and Visitor groups of a web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// Gets the associated visitors group of a web\nconst visitorGroup = await sp.web.associatedVisitorGroup();\n\n// Gets the associated members group of a web\nconst memberGroup = await sp.web.associatedMemberGroup();\n\n// Gets the associated owners group of a web\nconst ownerGroup = await sp.web.associatedOwnerGroup();\n\n
"},{"location":"sp/site-groups/#create-the-default-associated-groups-for-a-web","title":"Create the default associated groups for a web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// Breaks permission inheritance and creates the default associated groups for the web\n\n// Login name of the owner\nconst owner1 = \"owner@example.onmicrosoft.com\";\n\n// Specify true, the permissions should be copied from the current parent scope, else false\nconst copyRoleAssignments = false;\n\n// Specify true to make all child securable objects inherit role assignments from the current object\nconst clearSubScopes = true;\n\nawait sp.web.createDefaultAssociatedGroups(\"PnP Site\", owner1, copyRoleAssignments, clearSubScopes);\n
"},{"location":"sp/site-groups/#create-a-new-site-group","title":"Create a new site group","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\n// Creates a new site group with the specified title\nawait sp.web.siteGroups.add({\"Title\":\"new group name\"});\n
"},{"location":"sp/site-groups/#isitegroup","title":"ISiteGroup","text":"Scenario Import Statement Selective 2 import \"@pnp/sp/webs\";import \"@pnp/sp/site-groups\"; Selective 3 import \"@pnp/sp/webs\";import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups, SiteGroup } from \"@pnp/sp/presets/all\";"},{"location":"sp/site-groups/#getting-and-updating-the-groups-of-a-sharepoint-web","title":"Getting and updating the groups of a sharepoint web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups\";\n\nconst sp = spfi(...);\n\n// get the group using a group id\nconst groupID = 33;\nlet grp = await sp.web.siteGroups.getById(groupID)();\n\n// get the group using the group's name\nconst groupName = \"ClassicTeam Visitors\";\ngrp = await sp.web.siteGroups.getByName(groupName)();\n\n// update a group\nawait sp.web.siteGroups.getById(groupID).update({\"Title\": \"New Group Title\"});\n\n// delete a group from the site using group id\nawait sp.web.siteGroups.removeById(groupID);\n\n// delete a group from the site using group name\nawait sp.web.siteGroups.removeByLoginName(groupName);\n
"},{"location":"sp/site-groups/#getting-all-users-of-a-group","title":"Getting all users of a group","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups\";\n\nconst sp = spfi(...);\n\n// get all users of group\nconst groupID = 7;\nconst users = await sp.web.siteGroups.getById(groupID).users();\n
"},{"location":"sp/site-groups/#updating-the-owner-of-a-site-group","title":"Updating the owner of a site group","text":"

Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups\";\n\nconst sp = spfi(...);\n\n// Update the owner with a user id\nawait sp.web.siteGroups.getById(7).setUserAsOwner(4);\n
"},{"location":"sp/site-scripts/","title":"@pnp/sp/site-scripts","text":""},{"location":"sp/site-scripts/#create-a-new-site-script","title":"Create a new site script","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\nconst sitescriptContent = {\n    \"$schema\": \"schema.json\",\n    \"actions\": [\n        {\n            \"themeName\": \"Theme Name 123\",\n            \"verb\": \"applyTheme\",\n        },\n    ],\n    \"bindata\": {},\n    \"version\": 1,\n};\n\nconst siteScript = await sp.siteScripts.createSiteScript(\"Title\", \"description\", sitescriptContent);\n\nconsole.log(siteScript.Title);\n
"},{"location":"sp/site-scripts/#retrieval","title":"Retrieval","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\n// Retrieving all site scripts\nconst allSiteScripts = await sp.siteScripts.getSiteScripts();\nconsole.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : \"\");\n\n// Retrieving a single site script by Id\nconst siteScript = await sp.siteScripts.getSiteScriptMetadata(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\");\nconsole.log(siteScript.Title);\n
"},{"location":"sp/site-scripts/#update-and-delete","title":"Update and delete","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\n// Update\nconst updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\", Title: \"New Title\" });\nconsole.log(updatedSiteScript.Title);\n\n// Delete\nawait sp.siteScripts.deleteSiteScript(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\");\n
"},{"location":"sp/site-scripts/#get-site-script-from-a-list","title":"Get site script from a list","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\n// Using the absolute URL of the list\nconst ss = await sp.siteScripts.getSiteScriptFromList(\"https://TENANT.sharepoint.com/Lists/mylist\");\n\n// Using the PnPjs web object to fetch the site script from a specific list\nconst ss2 = await sp.web.lists.getByTitle(\"mylist\").getSiteScript();\n
"},{"location":"sp/site-scripts/#get-site-script-from-a-web","title":"Get site script from a web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst extractInfo = {\n    IncludeBranding: true,\n    IncludeLinksToExportedItems: true,\n    IncludeRegionalSettings: true,\n    IncludeSiteExternalSharingCapability: true,\n    IncludeTheme: true,\n    IncludedLists: [\"Lists/MyList\"]\n};\n\nconst ss = await sp.siteScripts.getSiteScriptFromWeb(\"https://TENANT.sharepoint.com/sites/mysite\", extractInfo);\n\n// Using the PnPjs web object to fetch the site script from a specific web\nconst ss2 = await sp.web.getSiteScript(extractInfo);\n
"},{"location":"sp/site-scripts/#execute-site-script-action","title":"Execute Site Script Action","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/site-scripts\";\n\nconst sp = spfi(...);\n\nconst siteScript = \"your site script action...\";\n\nconst ss = await sp.siteScripts.executeSiteScriptAction(siteScript);\n
"},{"location":"sp/site-scripts/#execute-site-script-for-a-specific-web","title":"Execute site script for a specific web","text":"
import { spfi } from \"@pnp/sp\";\nimport { SiteScripts } \"@pnp/sp/site-scripts\";\n\nconst siteScript = \"your site script action...\";\n\nconst scriptService = SiteScripts(\"https://absolute/url/to/web\");\n\nconst ss = await scriptService.executeSiteScriptAction(siteScript);\n
"},{"location":"sp/site-users/","title":"@pnp/sp/site-users","text":"

The site users module provides methods to manage users for a sharepoint site.

"},{"location":"sp/site-users/#isiteusers","title":"ISiteUsers","text":""},{"location":"sp/site-users/#get-all-site-user","title":"Get all site user","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst users = await sp.web.siteUsers();\n
"},{"location":"sp/site-users/#get-current-user","title":"Get Current user","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nlet user = await sp.web.currentUser();\n
"},{"location":"sp/site-users/#get-user-by-id","title":"Get user by id","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst id = 6;\nuser = await sp.web.getUserById(id)();\n
"},{"location":"sp/site-users/#ensure-user","title":"Ensure user","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst username = \"usernames@microsoft.com\";\nresult = await sp.web.ensureUser(username);\n
"},{"location":"sp/site-users/#isiteuser","title":"ISiteUser","text":"Scenario Import Statement Selective 2 import \"@pnp/sp/webs\";import \"@pnp/sp/site-users\"; Selective 3 import \"@pnp/sp/webs\";import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers, SiteUser } from \"@pnp/sp/presets/all\";"},{"location":"sp/site-users/#get-user-groups","title":"Get user Groups","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nlet groups = await sp.web.currentUser.groups();\n
"},{"location":"sp/site-users/#add-user-to-site-collection","title":"Add user to Site collection","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.ensureUser(\"userLoginname\")\nconst users = await sp.web.siteUsers;\n\nawait users.add(user.data.LoginName);\n
"},{"location":"sp/site-users/#get-user","title":"Get user","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\n// get user object by id\nconst user = await sp.web.siteUsers.getById(6)();\n\n//get user object by Email\nconst user = await sp.web.siteUsers.getByEmail(\"user@mail.com\")();\n\n//get user object by LoginName\nconst user = await sp.web.siteUsers.getByLoginName(\"userLoginName\")();\n
"},{"location":"sp/site-users/#update-user","title":"Update user","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nlet userProps = await sp.web.currentUser();\nuserProps.Title = \"New title\";\nawait sp.web.currentUser.update(userProps);\n
"},{"location":"sp/site-users/#remove-user","title":"Remove user","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\n// remove user by id\nawait sp.web.siteUsers.removeById(6);\n\n// remove user by LoginName\nawait sp.web.siteUsers.removeByLoginName(6);\n
"},{"location":"sp/site-users/#isiteuserprops","title":"ISiteUserProps","text":"

User properties:

Property Name Type Description Email string Contains Site user email Id Number Contains Site user Id IsHiddenInUI Boolean Site user IsHiddenInUI IsShareByEmailGuestUser boolean Site user is external user IsSiteAdmin Boolean Describes if Site user Is Site Admin LoginName string Site user LoginName PrincipalType number Site user Principal type Title string Site user Title
interface ISiteUserProps {\n\n    /**\n     * Contains Site user email\n     *\n     */\n    Email: string;\n\n    /**\n     * Contains Site user Id\n     *\n     */\n    Id: number;\n\n    /**\n     * Site user IsHiddenInUI\n     *\n     */\n    IsHiddenInUI: boolean;\n\n    /**\n     * Site user IsShareByEmailGuestUser\n     *\n     */\n    IsShareByEmailGuestUser: boolean;\n\n    /**\n     * Describes if Site user Is Site Admin\n     *\n     */\n    IsSiteAdmin: boolean;\n\n    /**\n     * Site user LoginName\n     *\n     */\n    LoginName: string;\n\n    /**\n     * Site user Principal type\n     *\n     */\n    PrincipalType: number | PrincipalType;\n\n    /**\n     * Site user Title\n     *\n     */\n    Title: string;\n}\n
"},{"location":"sp/sites/","title":"@pnp/sp/site - Site properties","text":"

Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.

"},{"location":"sp/sites/#get-context-information-for-the-current-site-collection","title":"Get context information for the current site collection","text":"

Using the library, you can get the context information of the current site collection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport { IContextInfo } from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst oContext: IContextInfo = await sp.site.getContextInfo();\nconsole.log(oContext.FormDigestValue);\n
"},{"location":"sp/sites/#get-document-libraries-of-a-web","title":"Get document libraries of a web","text":"

Using the library, you can get a list of the document libraries present in the a given web.

Note: Works only in SharePoint online

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport { IDocumentLibraryInformation } from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries(\"https://tenant.sharepoint.com/sites/test/subsite\");\n\n//we got the array of document library information\ndocLibs.forEach((docLib: IDocumentLibraryInformation) => {\n    // do something with each library\n});\n
"},{"location":"sp/sites/#open-web-by-id","title":"Open Web By Id","text":"

Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst w = await sp.site.openWebById(\"111ca453-90f5-482e-a381-cee1ff383c9e\");\n\n//we got all the data from the web as well\nconsole.log(w.data);\n\n// we can chain\nconst w2 = await w.web.select(\"Title\")();\n
"},{"location":"sp/sites/#get-absolute-web-url-from-page-url","title":"Get absolute web url from page url","text":"

Using the library, you can get the absolute web url by providing a page url

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst d: string = await sp.site.getWebUrlFromPageUrl(\"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\");\n\nconsole.log(d); //https://tenant.sharepoint.com/sites/test\n
"},{"location":"sp/sites/#access-the-root-web","title":"Access the root web","text":"

There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. \"_api/sites/rootweb\" which does not work for all operations.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// use for rootweb information access\nconst rootwebData = await sp.site.rootWeb();\n\n// use for chaining\nconst rootweb = await sp.site.getRootWeb();\nconst listData = await rootWeb.lists.getByTitle(\"MyList\")();\n
"},{"location":"sp/sites/#create-a-modern-communication-site","title":"Create a modern communication site","text":"

Note: Works only in SharePoint online

Creates a modern communication site.

Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site Owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com
\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst result = await sp.site.createCommunicationSite(\n            \"Title\",\n            1033,\n            true,\n            \"https://tenant.sharepoint.com/sites/commSite\",\n            \"Description\",\n            \"HBI\",\n            \"f6cc5403-0d63-442e-96c0-285923709ffc\",\n            \"a00ec589-ea9f-4dba-a34e-67e78d41e509\",\n            \"user@TENANT.onmicrosoft.com\");\n\n
"},{"location":"sp/sites/#create-from-props","title":"Create from Props","text":"

You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// in this case you supply a single struct deinfing the creation props\nconst result = await sp.site.createCommunicationSiteFromProps({\n  Owner: \"patrick@three18studios.com\",\n  Title: \"A Test Site\",\n  Url: \"https://{tenant}.sharepoint.com/sites/commsite2\",\n  WebTemplate: \"STS#3\",\n});\n
"},{"location":"sp/sites/#create-a-modern-team-site","title":"Create a modern team site","text":"

Note: Works only in SharePoint online. It wont work with App only tokens

Creates a modern team site backed by O365 group.

Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc
\nimport { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\nconst result = await sp.site.createModernTeamSite(\n        \"displayName\",\n        \"alias\",\n        true,\n        1033,\n        \"description\",\n        \"HBI\",\n        [\"user1@tenant.onmicrosoft.com\",\"user2@tenant.onmicrosoft.com\",\"user3@tenant.onmicrosoft.com\"],\n        \"a00ec589-ea9f-4dba-a34e-67e78d41e509\",\n        \"f6cc5403-0d63-442e-96c0-285923709ffc\"\n        );\n\nconsole.log(d);\n
"},{"location":"sp/sites/#create-from-props_1","title":"Create from Props","text":"

You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// in this case you supply a single struct deinfing the creation props\nconst result = await sp.site.createModernTeamSiteFromProps({\n  alias: \"JenniferGarner\",\n  displayName: \"A Test Site\",\n  owners: [\"patrick@three18studios.com\"],\n});\n
"},{"location":"sp/sites/#delete-a-site-collection","title":"Delete a site collection","text":"

Using the library, you can delete a specific site collection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport { Site } from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// Delete the current site\nawait sp.site.delete();\n\n// Specify which site to delete\nconst siteUrl = \"https://tenant.sharepoint.com/sites/subsite\";\nconst site2 = Site(siteUrl);\nawait site2.delete();\n
"},{"location":"sp/sites/#check-if-a-site-collection-exists","title":"Check if a Site Collection Exists","text":"

Using the library, you can check if a specific site collection exist or not on your tenant

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n// Specify which site to verify\nconst siteUrl = \"https://tenant.sharepoint.com/sites/subsite\";\nconst exists = await sp.site.exists(siteUrl);\nconsole.log(exists);\n
"},{"location":"sp/sites/#set-the-site-logo","title":"Set the site logo","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sites\";\nimport {ISiteLogoProperties, SiteLogoAspect, SiteLogoType} from \"@pnp/sp/sites\";\n\nconst sp = spfi(...);\n\n//set the web's site logo\nconst logoProperties: ISiteLogoProperties = {\n    relativeLogoUrl: \"/sites/mySite/SiteAssets/site_logo.png\", \n    aspect: SiteLogoAspect.Rectangular, \n    type: SiteLogoType.WebLogo\n};\nawait sp.site.setSiteLogo(logoProperties);\n
"},{"location":"sp/social/","title":"@pnp/sp/ - social","text":"

The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions.

"},{"location":"sp/social/#getfollowedsitesuri","title":"getFollowedSitesUri","text":"

Gets a URI to a site that lists the current user's followed sites.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\nconst uri = await sp.social.getFollowedSitesUri();\n
"},{"location":"sp/social/#getfolloweddocumentsuri","title":"getFollowedDocumentsUri","text":"

Gets a URI to a site that lists the current user's followed documents.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\nconst uri = await sp.social.getFollowedDocumentsUri();\n
"},{"location":"sp/social/#follow","title":"follow","text":"

Makes the current user start following a user, document, site, or tag

import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// follow a site\nconst r1 = await sp.social.follow({\n    ActorType: SocialActorType.Site,\n    ContentUri: \"htts://tenant.sharepoint.com/sites/site\",\n});\n\n// follow a person\nconst r2 = await sp.social.follow({\n    AccountName: \"i:0#.f|membership|person@tenant.com\",\n    ActorType: SocialActorType.User,\n});\n\n// follow a doc\nconst r3 = await sp.social.follow({\n    ActorType: SocialActorType.Document,\n    ContentUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\",\n});\n\n// follow a tag\n// You need the tag GUID to start following a tag.\n// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model.\n// See How to get a tag's GUID based on the tag's name by using the JavaScript object model.\n// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid\nconst r4 = await sp.social.follow({\n    ActorType: SocialActorType.Tag,\n    TagGuid: \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\",\n});\n
"},{"location":"sp/social/#isfollowed","title":"isFollowed","text":"

Indicates whether the current user is following a specified user, document, site, or tag

import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// pass the same social actor struct as shown in follow example for each type\nconst r = await sp.social.isFollowed({\n    AccountName: \"i:0#.f|membership|person@tenant.com\",\n    ActorType: SocialActorType.User,\n});\n
"},{"location":"sp/social/#stopfollowing","title":"stopFollowing","text":"

Makes the current user stop following a user, document, site, or tag

import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// pass the same social actor struct as shown in follow example for each type\nconst r = await sp.social.stopFollowing({\n    AccountName: \"i:0#.f|membership|person@tenant.com\",\n    ActorType: SocialActorType.User,\n});\n
"},{"location":"sp/social/#my","title":"my","text":""},{"location":"sp/social/#get","title":"get","text":"

Gets this user's social information

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\nconst r = await sp.social.my();\n
"},{"location":"sp/social/#followed","title":"followed","text":"

Gets users, documents, sites, and tags that the current user is following based on the supplied flags.

import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get all the followed documents\nconst r1 = await sp.social.my.followed(SocialActorTypes.Document);\n\n// get all the followed documents and sites\nconst r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site);\n\n// get all the followed sites updated in the last 24 hours\nconst r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);\n
"},{"location":"sp/social/#followedcount","title":"followedCount","text":"

Works as followed but returns on the count of actors specified by the query

import { spfi } from \"@pnp/sp\";\nimport { SocialActorType } from \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get the followed documents count\nconst r = await sp.social.my.followedCount(SocialActorTypes.Document);\n
"},{"location":"sp/social/#followers","title":"followers","text":"

Gets the users who are following the current user.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get the followed documents count\nconst r = await sp.social.my.followers();\n
"},{"location":"sp/social/#suggestions","title":"suggestions","text":"

Gets users who the current user might want to follow.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/social\";\n\nconst sp = spfi(...);\n\n// get the followed documents count\nconst r = await sp.social.my.suggestions();\n
"},{"location":"sp/sp-utilities-utility/","title":"@pnp/sp/utilities","text":"

Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.

"},{"location":"sp/sp-utilities-utility/#sendemail","title":"sendEmail","text":"

This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).

"},{"location":"sp/sp-utilities-utility/#emailproperties","title":"EmailProperties","text":"
export interface TypedHash<T> {\n    [key: string]: T;\n}\n\nexport interface EmailProperties {\n\n    To: string[];\n    CC?: string[];\n    BCC?: string[];\n    Subject: string;\n    Body: string;\n    AdditionalHeaders?: TypedHash<string>;\n    From?: string;\n}\n
"},{"location":"sp/sp-utilities-utility/#usage","title":"Usage","text":"

You must define the To, Subject, and Body values - the remaining are optional.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\nimport { IEmailProperties } from \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nconst emailProps: IEmailProperties = {\n    To: [\"user@site.com\"],\n    CC: [\"user2@site.com\", \"user3@site.com\"],\n    BCC: [\"user4@site.com\", \"user5@site.com\"],\n    Subject: \"This email is about...\",\n    Body: \"Here is the body. <b>It supports html</b>\",\n    AdditionalHeaders: {\n        \"content-type\": \"text/html\"\n    }\n};\n\nawait sp.utility.sendEmail(emailProps);\nconsole.log(\"Email Sent!\");\n
"},{"location":"sp/sp-utilities-utility/#getcurrentuseremailaddresses","title":"getCurrentUserEmailAddresses","text":"

This method returns the current user's email addresses known to SharePoint.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet addressString: string = await sp.utility.getCurrentUserEmailAddresses();\n\n// and use it with sendEmail\nawait sp.utility.sendEmail({\n    To: [addressString],\n    Subject: \"This email is about...\",\n    Body: \"Here is the body. <b>It supports html</b>\",\n    AdditionalHeaders: {\n        \"content-type\": \"text/html\"\n    },\n});\n
"},{"location":"sp/sp-utilities-utility/#resolveprincipal","title":"resolvePrincipal","text":"

Gets information about a principal that matches the specified Search criteria

import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet principal : IPrincipalInfo = await sp.utility.resolvePrincipal(\"user@site.com\", PrincipalType.User, PrincipalSource.All, true, false, true);\n\nconsole.log(principal);\n
"},{"location":"sp/sp-utilities-utility/#searchprincipals","title":"searchPrincipals","text":"

Gets information about the principals that match the specified Search criteria.

import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet principals : IPrincipalInfo[] = await sp.utility.searchPrincipals(\"john\", PrincipalType.User, PrincipalSource.All,\"\", 10);\n\nconsole.log(principals);\n
"},{"location":"sp/sp-utilities-utility/#createemailbodyforinvitation","title":"createEmailBodyForInvitation","text":"

Gets the external (outside the firewall) URL to a document or resource in a site.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet url : string = await sp.utility.createEmailBodyForInvitation(\"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\");\nconsole.log(url);\n
"},{"location":"sp/sp-utilities-utility/#expandgroupstoprincipals","title":"expandGroupsToPrincipals","text":"

Resolves the principals contained within the supplied groups

import { spfi, SPFx, IPrincipalInfo } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"]);\nconsole.log(principals);\n\n// optionally supply a max results count. Default is 30.\nlet principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"], 10);\nconsole.log(principals);\n
"},{"location":"sp/sp-utilities-utility/#createwikipage","title":"createWikiPage","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/sputilities\";\nimport { ICreateWikiPageResult } from \"@pnp/sp/sputilities\";\n\nconst sp = spfi(...);\n\nlet newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({\n    ServerRelativeUrl: \"/sites/dev/SitePages/mynewpage.aspx\",\n    WikiHtmlContent: \"This is my <b>page</b> content. It supports rich html.\",\n});\n\n// newPage contains the raw data returned by the service\nconsole.log(newPage.data);\n\n// newPage contains a File instance you can use to further update the new page\nlet file = await newPage.file();\nconsole.log(file);\n
"},{"location":"sp/subscriptions/","title":"@pnp/sp/subscriptions","text":"

Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.

"},{"location":"sp/subscriptions/#isubscriptions","title":"ISubscriptions","text":""},{"location":"sp/subscriptions/#add-a-webhook","title":"Add a webhook","text":"

Using this library, you can add a webhook to a specified list within the SharePoint site.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\n\nimport { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\";\nimport \"@pnp/sp/subscriptions/list\";\n\nconst sp = spfi(...);\n\n// This is the URL which will be called by SharePoint when there is a change in the list\nconst notificationUrl = \"<notification-url>\";\n\n// Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date.\nconst expiryDate = dateAdd(new Date(), \"day\" , 180).toISOString();\n\n// Adds a webhook to the Documents library\nvar res = await sp.web.lists.getByTitle(\"Documents\").subscriptions.add(notificationUrl,expiryDate);\n
"},{"location":"sp/subscriptions/#get-all-webhooks-added-to-a-list","title":"Get all webhooks added to a list","text":"

Read all the webhooks' details which are associated to the list

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/subscriptions\";\n\nconst sp = spfi(...);\n\nconst res = await sp.web.lists.getByTitle(\"Documents\").subscriptions();\n
"},{"location":"sp/subscriptions/#isubscription","title":"ISubscription","text":"

This interface provides the methods for managing a particular webhook.

Scenario Import Statement Selective import \"@pnp/sp/webs\";import \"@pnp/sp/lists\";import { Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/subscriptions\";import \"@pnp/sp/subscriptions/list\" Preset: All import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from \"@pnp/sp/presets/all\";"},{"location":"sp/subscriptions/#managing-a-webhook","title":"Managing a webhook","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/subscriptions\";\n\nconst sp = spfi(...);\n\n// Get details of a webhook based on its ID\nconst webhookId = \"1f029e5c-16e4-4941-b46f-67895118763f\";\nconst webhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId)();\n\n// Update a webhook\nconst newDate = dateAdd(new Date(), \"day\" , 150).toISOString();\nconst updatedWebhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).update(newDate);\n\n// Delete a webhook\nawait sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).delete();\n
"},{"location":"sp/taxonomy/","title":"@pnp/sp/taxonomy","text":"

Provides access to the v2.1 api term store

"},{"location":"sp/taxonomy/#docs-updated-with-v209-release-as-the-underlying-api-changed","title":"Docs updated with v2.0.9 release as the underlying API changed","text":"

NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabilize this note will be removed.

"},{"location":"sp/taxonomy/#term-store","title":"Term Store","text":"

Access term store data from the root sp object as shown below.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermStoreInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term store data\nconst info: ITermStoreInfo = await sp.termStore();\n
"},{"location":"sp/taxonomy/#searchterm","title":"searchTerm","text":"

Added in 3.3.0

Search for terms starting with provided label under entire termStore or a termSet or a parent term.

The following properties are valid for the supplied query: label: string, setId?: string, parentTermId?: string, languageTag?: string, stringMatchOption?: \"ExactMatch\" | \"StartsWith\".

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// minimally requires the label\nconst results1 = await sp.termStore.searchTerm({\n  label: \"test\",\n});\n\n// other properties can be included as needed\nconst results2 = await sp.termStore.searchTerm({\n  label: \"test\",\n  setId: \"{guid}\",\n});\n\n// other properties can be included as needed\nconst results3 = await sp.termStore.searchTerm({\n  label: \"test\",\n  setId: \"{guid}\",\n  stringMatchOption: \"ExactMatch\",\n});\n
"},{"location":"sp/taxonomy/#update","title":"update","text":"

Added in 3.10.0

Allows you to update language setttings for the store

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\nawait sp.termStore.update({\n  defaultLanguageTag: \"en-US\",\n  languageTags: [\"en-US\", \"en-IE\", \"de-DE\"],\n});\n
"},{"location":"sp/taxonomy/#term-groups","title":"Term Groups","text":"

Access term group information

"},{"location":"sp/taxonomy/#list","title":"List","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term groups\nconst info: ITermGroupInfo[] = await sp.termStore.groups();\n
"},{"location":"sp/taxonomy/#get-by-id","title":"Get By Id","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term groups data\nconst info: ITermGroupInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\")();\n
"},{"location":"sp/taxonomy/#add","title":"Add","text":"

Added in 3.10.0

Allows you to add a term group to a store.

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\nconst groupInfo: ITermGroupInfo = await sp.termStore.groups.add({\n  displayName: \"Accounting\",\n  description: \"Term Group for Accounting\",\n  name: \"accounting1\",\n  scope: \"global\",\n});\n
"},{"location":"sp/taxonomy/#term-group","title":"Term Group","text":""},{"location":"sp/taxonomy/#delete","title":"Delete","text":"

Added in 3.10.0

Allows you to add a term group to a store.

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nawait sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n
"},{"location":"sp/taxonomy/#term-sets","title":"Term Sets","text":"

Access term set information

"},{"location":"sp/taxonomy/#list_1","title":"List","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermSetInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get set info\nconst info: ITermSetInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets();\n
"},{"location":"sp/taxonomy/#get-by-id_1","title":"Get By Id","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermSetInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term set data by group id then by term set id\nconst info: ITermSetInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")();\n\n// get term set data by term set id\nconst infoByTermSetId: ITermSetInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")();\n
"},{"location":"sp/taxonomy/#add_1","title":"Add","text":"

Added in 3.10.0

Allows you to add a term set.

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\n// when adding a set directly from the root .sets property, you must include the \"parentGroup\" property\nconst setInfo = await sp.termStore.sets.add({\n  parentGroup: {\n    id: \"338666a8-1111-2222-3333-f72471314e72\"\n  },\n  contact: \"steve\",\n  description: \"description\",\n  isAvailableForTagging: true,\n  isOpen: true,\n  localizedNames: [{\n    name: \"MySet\",\n    languageTag: \"en-US\",\n  }],\n  properties: [{\n    key: \"key1\",\n    value: \"value1\",\n  }]\n});\n\n// when adding a termset through a group's sets property you do not specify the \"parentGroup\" property\nconst setInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.add({\n  contact: \"steve\",\n  description: \"description\",\n  isAvailableForTagging: true,\n  isOpen: true,\n  localizedNames: [{\n    name: \"MySet2\",\n    languageTag: \"en-US\",\n  }],\n  properties: [{\n    key: \"key1\",\n    value: \"value1\",\n  }]\n});\n
"},{"location":"sp/taxonomy/#getallchildrenasorderedtree","title":"getAllChildrenAsOrderedTree","text":"

This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change.

Starting with version 2.6.0 you can now include an optional param to retrieve all of the term's properties and localProperties in the tree. Default is false.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\nimport { dateAdd, PnPClientStorage } from \"@pnp/core\";\n\nconst sp = spfi(...);\n\n// here we get all the children of a given set\nconst childTree = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree();\n\n// here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available\nconst store = new PnPClientStorage();\n\n// our tree likely doesn't change much in 30 minutes for most applications\n// adjust to be longer or shorter as needed\nconst cachedTree = await store.local.getOrPut(\"myKey\", () => {\n    return sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree();\n}, dateAdd(new Date(), \"minute\", 30));\n\n// you can also get all the properties and localProperties\nconst set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\");\nconst childTree = await set.getAllChildrenAsOrderedTree({ retrieveProperties: true });\n
"},{"location":"sp/taxonomy/#termset","title":"TermSet","text":"

Access term set information

"},{"location":"sp/taxonomy/#update_1","title":"Update","text":"

Added in 3.10.0

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst termSetInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n  properties: [{\n    key: \"MyKey2\",\n    value: \"MyValue2\",\n  }],\n});\n\nconst termSetInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n  properties: [{\n    key: \"MyKey3\",\n    value: \"MyValue3\",\n  }],\n});\n
"},{"location":"sp/taxonomy/#delete_1","title":"Delete","text":"

Added in 3.10.0

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermGroupInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nawait sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n\nawait sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n
"},{"location":"sp/taxonomy/#terms","title":"Terms","text":"

Access term set information

"},{"location":"sp/taxonomy/#list_2","title":"List","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// list all the terms that are direct children of this set\nconst infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children();\n
"},{"location":"sp/taxonomy/#list-terms","title":"List (terms)","text":"

You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// list all the terms available in this term set by group id then by term set id\nconst infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms();\n\n// list all the terms available in this term set by term set id\nconst infosByTermSetId: ITermInfo[] = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms();\n
"},{"location":"sp/taxonomy/#get-by-id_2","title":"Get By Id","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get term set data\nconst info: ITermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\")();\n
"},{"location":"sp/taxonomy/#add_2","title":"Add","text":"

Added in 3.10.0

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\nimport { ITermInfo } from \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst newTermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children.add({\n  labels: [\n    {\n      isDefault: true,\n      languageTag: \"en-us\",\n      name: \"New Term\",\n    }\n  ]\n});\n\nconst newTermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children.add({\n  labels: [\n    {\n      isDefault: true,\n      languageTag: \"en-us\",\n      name: \"New Term 2\",\n    }\n  ]\n});\n
"},{"location":"sp/taxonomy/#term","title":"Term","text":""},{"location":"sp/taxonomy/#update_2","title":"Update","text":"

Note that when updating a Term if you update the properties it replaces the collection, so a merge of existing + new needs to be handled by your application.

Added in 3.10.0

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst termInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n  properties: [{\n    key: \"something\",\n    value: \"a value 2\",\n  }],\n});\n\nconst termInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").update({\n  properties: [{\n    key: \"something\",\n    value: \"a value\",\n  }],\n});\n
"},{"location":"sp/taxonomy/#delete_2","title":"Delete","text":"

Added in 3.10.0

import { spfi, SPFxToken, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\n// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.\n// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`\nconst sp = spfi().using(SPFx(context), SPFxToken(context));\n\nconst termInfo = await sp.termStore.sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n\nconst termInfo2 = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\").delete();\n
"},{"location":"sp/taxonomy/#get-term-parent","title":"Get Term Parent","text":"

Behavior Change in 2.1.0

The server API changed again, resulting in the removal of the \"parent\" property from ITerm as it is not longer supported as a path property. You now must use \"expand\" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/taxonomy\";\n\nconst sp = spfi(...);\n\n// get a ref to the set\nconst set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\");\n\n// get a term's information and expand parent to get the parent info as well\nconst w = await set.getTermById(\"338666a8-1111-2222-3333-f72471314e72\").expand(\"parent\")();\n\n// get a ref to the parent term\nconst parent = set.getTermById(w.parent.id);\n\n// make a request for the parent term's info - this data currently match the results in the expand call above, but this\n// is to demonstrate how to gain a ref to the parent and select its data\nconst parentInfo = await parent.select(\"Id\", \"Descriptions\")();\n
"},{"location":"sp/tenant-properties/","title":"@pnp/sp/web - tenant properties","text":"

You can set, read, and remove tenant properties using the methods shown below:

"},{"location":"sp/tenant-properties/#setstorageentity","title":"setStorageEntity","text":"

This method MUST be called in the context of the app catalog web or you will get an access denied message.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst w = await sp.getTenantAppCatalogWeb();\n\n// specify required key and value\nawait w.setStorageEntity(\"Test1\", \"Value 1\");\n\n// specify optional description and comments\nawait w.setStorageEntity(\"Test2\", \"Value 2\", \"description\", \"comments\");\n
"},{"location":"sp/tenant-properties/#getstorageentity","title":"getStorageEntity","text":"

This method can be used from any web to retrieve values previously set.

import { spfi, SPFx } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\nimport { IStorageEntity } from \"@pnp/sp/webs\"; \n\nconst sp = spfi(...);\n\nconst prop: IStorageEntity = await sp.web.getStorageEntity(\"Test1\");\n\nconsole.log(prop.Value);\n
"},{"location":"sp/tenant-properties/#removestorageentity","title":"removeStorageEntity","text":"

This method MUST be called in the context of the app catalog web or you will get an access denied message.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/appcatalog\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst w = await sp.getTenantAppCatalogWeb();\n\nawait w.removeStorageEntity(\"Test1\");\n
"},{"location":"sp/user-custom-actions/","title":"@pnp/sp/user-custom-actions","text":"

Represents a custom action associated with a SharePoint list, web or site collection.

"},{"location":"sp/user-custom-actions/#iusercustomactions","title":"IUserCustomActions","text":""},{"location":"sp/user-custom-actions/#get-a-collection-of-user-custom-actions-from-a-web","title":"Get a collection of User Custom Actions from a web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\nconst userCustomActions = sp.web.userCustomActions();\n
"},{"location":"sp/user-custom-actions/#add-a-new-user-custom-action","title":"Add a new User Custom Action","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\nimport { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions';\n\nconst sp = spfi(...);\n\nconst newValues: TypedHash<string> = {\n    \"Title\": \"New Title\",\n    \"Description\": \"New Description\",\n    \"Location\": \"ScriptLink\",\n    \"ScriptSrc\": \"https://...\"\n};\n\nconst response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);\n
"},{"location":"sp/user-custom-actions/#get-a-user-custom-action-by-id","title":"Get a User Custom Action by ID","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\nconst uca: IUserCustomAction = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\");\n\nconst ucaData = await uca();\n
"},{"location":"sp/user-custom-actions/#clear-the-user-custom-action-collection","title":"Clear the User Custom Action collection","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\n// Site collection level\nawait sp.site.userCustomActions.clear();\n\n// Site (web) level\nawait sp.web.userCustomActions.clear();\n\n// List level\nawait sp.web.lists.getByTitle(\"Documents\").userCustomActions.clear();\n
"},{"location":"sp/user-custom-actions/#iusercustomaction","title":"IUserCustomAction","text":""},{"location":"sp/user-custom-actions/#update-an-existing-user-custom-action","title":"Update an existing User Custom Action","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions\";\nimport { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions';\n\nconst sp = spfi(...);\n\nconst uca = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\");\n\nconst newValues: TypedHash<string> = {\n    \"Title\": \"New Title\",\n    \"Description\": \"New Description\",\n    \"ScriptSrc\": \"https://...\"\n};\n\nconst response: IUserCustomActionUpdateResult = uca.update(newValues);\n
"},{"location":"sp/views/","title":"@pnp/sp/views","text":"

Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.

"},{"location":"sp/views/#iviews","title":"IViews","text":""},{"location":"sp/views/#get-views-in-a-list","title":"Get views in a list","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// get all the views and their properties\nconst views1 = await list.views();\n\n// you can use odata select operations to get just a set a fields\nconst views2 = await list.views.select(\"Id\", \"Title\")();\n\n// get the top three views\nconst views3 = await list.views.top(3)();\n
"},{"location":"sp/views/#add-a-view","title":"Add a View","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\n// create a new view with default fields and properties\nconst result = await list.views.add(\"My New View\");\n\n// create a new view with specific properties\nconst result2 = await list.views.add(\"My New View 2\", false, {\n    RowLimit: 10,\n    ViewQuery: \"<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>\",\n});\n\n// manipulate the view's fields\nawait result2.view.fields.removeAll();\n\nawait Promise.all([\n    result2.view.fields.add(\"Title\"),\n    result2.view.fields.add(\"Modified\"),\n]);\n
"},{"location":"sp/views/#iview","title":"IView","text":""},{"location":"sp/views/#get-a-views-information","title":"Get a View's Information","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\nconst result = await list.views.getById(\"{GUID view id}\")();\n\nconst result2 = await list.views.getByTitle(\"My View\")();\n\nconst result3 = await list.views.getByTitle(\"My View\").select(\"Id\", \"Title\")();\n\nconst result4 = await list.defaultView();\n\nconst result5 = await list.getView(\"{GUID view id}\")();\n
"},{"location":"sp/views/#fields","title":"fields","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\nconst result = await list.views.getById(\"{GUID view id}\").fields();\n
"},{"location":"sp/views/#update","title":"update","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst list = sp.web.lists.getByTitle(\"My List\");\n\nconst result = await list.views.getById(\"{GUID view id}\").update({\n    RowLimit: 20,\n});\n
"},{"location":"sp/views/#renderashtml","title":"renderAsHtml","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").renderAsHtml();\n
"},{"location":"sp/views/#setviewxml","title":"setViewXml","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst viewXml = \"...\";\n\nawait sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").setViewXml(viewXml);\n
"},{"location":"sp/views/#delete","title":"delete","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst viewXml = \"...\";\n\nawait sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").delete();\n
"},{"location":"sp/views/#viewfields","title":"ViewFields","text":""},{"location":"sp/views/#getschemaxml","title":"getSchemaXml","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nconst xml = await sp.web.lists.getByTitle(\"My List\").defaultView.fields.getSchemaXml();\n
"},{"location":"sp/views/#add","title":"add","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.add(\"Created\");\n
"},{"location":"sp/views/#move","title":"move","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.move(\"Created\", 0);\n
"},{"location":"sp/views/#remove","title":"remove","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.remove(\"Created\");\n
"},{"location":"sp/views/#removeall","title":"removeAll","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/views\";\n\nconst sp = spfi(...);\n\nawait sp.web.lists.getByTitle(\"My List\").defaultView.fields.removeAll();\n
"},{"location":"sp/webs/","title":"@pnp/sp/webs","text":"

Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.

"},{"location":"sp/webs/#iwebs","title":"IWebs","text":""},{"location":"sp/webs/#add-web","title":"Add Web","text":"

Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions.

import { spfi } from \"@pnp/sp\";\nimport { IWebAddResult } from \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst result = await sp.web.webs.add(\"title\", \"subweb1\");\n\n// show the response from the server when adding the web\nconsole.log(result.data);\n\n// we can immediately operate on the new web\nresult.web.select(\"Title\")().then((w: IWebInfo)  => {\n\n    // show our title\n    console.log(w.Title);\n});\n
import { spfi } from \"@pnp/sp\";\nimport { IWebAddResult } from \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// create a German language wiki site with title, url, description, which does not inherit permissions\nsp.web.webs.add(\"wiki\", \"subweb2\", \"a wiki web\", \"WIKI#0\", 1031, false).then((w: IWebAddResult) => {\n\n  // ...\n});\n
"},{"location":"sp/webs/#iweb","title":"IWeb","text":""},{"location":"sp/webs/#access-a-web","title":"Access a Web","text":"

There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named \"web\" which represents an IWeb instance - regardless of how it was initially accessed.

Access the web from the imported \"spfi\" object using selective import:

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst r = await sp.web();\n

Access the web from the imported \"spfi\" object using the 'all' preset

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/presets/all\";\n\nconst sp = spfi(...);\n\nconst r = await sp.web();\n

Access the web using any SPQueryable as a base

In this scenario you might be deep in your code without access to the original start of the fluid chain (i.e. the instance produced from spfi). You can pass any queryable to the Web or Site factory and get back a valid IWeb instance. In this case all of the observers registered to the supplied instance will be referenced by the IWeb, and the url will be rebased to ensure a valid path.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists\";\nimport \"@pnp/sp/items\";\n\nconst sp = spfi(...);\n\n// we have a ref to the IItems instance\nconst items = await sp.web.lists.getByTitle(\"Generic\").items;\n\n// we create a new IWeb instance using the items as a base\nconst web = Web(items);\n\n// gets the web info\nconst webInfo = await web();\n\n// get a reference to a different list\nconst list = web.lists.getByTitle(\"DifferentList\");\n

Access a web using the Web factory method

There are several ways to use the Web factory directly and have some special considerations unique to creating IWeb instances from Web. The easiest is to supply the absolute URL of the web you wish to target, as seen in the first example below. When supplying a path parameter to Web you need to include the _api/web part in the appropriate location as the library can't from strings determine how to append the path. Example 2 below shows a wrong usage of the Web factory as we cannot determine how the path part should be appended. Examples 3 and 4 show how to include the _api/web part for both subwebs or queries within the given web.

When in doubt, supply the absolute url to the web as the first parameter as shown in example 1 below

import { spfi } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\n// creates a web:\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/_api/web\"\n// - has no registered observers\nconst web1 = Web(\"https://tenant.sharepoint.com/sites/myweb\");\n\n// creates a web that will not work due to missing the _api/web portion\n// this is because we don't know that the extra path should come before/after the _api/web portion\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb/some sub path\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/some sub path\"\n// - has no registered observers\nconst web2-WRONG = Web(\"https://tenant.sharepoint.com/sites/myweb\", \"some sub path\");\n\n// creates a web:\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb/some sub path\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/some sub web/_api/web\"\n// including the _api/web ensures the path you are providing is correct and can be parsed by the library\n// - has no registered observers\nconst web3 = Web(\"https://tenant.sharepoint.com/sites/myweb\", \"some sub web/_api/web\");\n\n// creates a web that actually points to the lists endpoint:\n// - whose root is \"https://tenant.sharepoint.com/sites/myweb/\"\n// - whose request path is \"https://tenant.sharepoint.com/sites/myweb/_api/web/lists\"\n// including the _api/web ensures the path you are providing is correct and can be parsed by the library\n// - has no registered observers\nconst web4 = Web(\"https://tenant.sharepoint.com/sites/myweb\", \"_api/web/lists\");\n

The above examples show you how to use the constructor to create the base url for the Web although none of them are usable as is until you add observers. You can do so by either adding them explicitly with a using...

import { spfi, SPFx } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\n\nconst web1 = Web(\"https://tenant.sharepoint.com/sites/myweb\").using(SPFx(this.context));\n

or by copying them from another SPQueryable instance...

import { spfi } from \"@pnp/sp\";\nimport { Web } from \"@pnp/sp/webs\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n//sp.web is of type SPQueryable; using tuple pattern pass SPQueryable and the web's url\nconst web = Web([sp.web, \"https://tenant.sharepoint.com/sites/otherweb\"]);\n
"},{"location":"sp/webs/#webs","title":"webs","text":"

Access the child webs collection of this web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst web = sp.web;\nconst webs = await web.webs();\n
"},{"location":"sp/webs/#get-a-webs-properties","title":"Get A Web's properties","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\n// basic get of the webs properties\nconst props = await sp.web();\n\n// use odata operators to get specific fields\nconst props2 = await sp.web.select(\"Title\")();\n\n// type the result to match what you are requesting\nconst props3 = await sp.web.select(\"Title\")<{ Title: string }>();\n
"},{"location":"sp/webs/#getparentweb","title":"getParentWeb","text":"

Get the data and IWeb instance for the parent web for the given web instance

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = web.getParentWeb();\n
"},{"location":"sp/webs/#getsubwebsfilteredforcurrentuser","title":"getSubwebsFilteredForCurrentUser","text":"

Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst web = sp.web;\nconst subWebs = web.getSubwebsFilteredForCurrentUser()();\n\n// apply odata operations to the collection\nconst subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select(\"Title\", \"Language\").orderBy(\"Created\", true)();\n

Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.

"},{"location":"sp/webs/#allproperties","title":"allProperties","text":"

Allows access to the web's all properties collection. This is readonly in REST.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\n\nconst web = sp.web;\nconst props = await web.allProperties();\n\n// select certain props\nconst props2 = await web.allProperties.select(\"prop1\", \"prop2\")();\n
"},{"location":"sp/webs/#webinfos","title":"webinfos","text":"

Gets a collection of WebInfos for this web's subwebs

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n\nconst infos = await web.webinfos();\n\n// or select certain fields\nconst infos2 = await web.webinfos.select(\"Title\", \"Description\")();\n\n// or filter\nconst infos3 = await web.webinfos.filter(\"Title eq 'MyWebTitle'\")();\n\n// or both\nconst infos4 = await web.webinfos.select(\"Title\", \"Description\").filter(\"Title eq 'MyWebTitle'\")();\n\n// get the top 4 ordered by Title\nconst infos5 = await web.webinfos.top(4).orderBy(\"Title\")();\n

Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.

"},{"location":"sp/webs/#update","title":"update","text":"

Updates this web instance with the supplied properties

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n// update the web's title and description\nconst result = await web.update({\n    Title: \"New Title\",\n    Description: \"My new description\",\n});\n\n// a project implementation could wrap the update to provide type information for your expected fields:\n\ninterface IWebUpdateProps {\n    Title: string;\n    Description: string;\n}\n\nfunction updateWeb(props: IWebUpdateProps): Promise<void> {\n    web.update(props);\n}\n
"},{"location":"sp/webs/#delete-a-web","title":"Delete a Web","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n\nawait web.delete();\n
"},{"location":"sp/webs/#applytheme","title":"applyTheme","text":"

Applies the theme specified by the contents of each of the files specified in the arguments to the site

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { combine } from \"@pnp/core\";\n\nconst sp = spfi(\"https://{tenant}.sharepoint.com/sites/dev/subweb\").using(SPFx(this.context));\nconst web = sp.web;\n\n// the urls to the color and font need to both be from the catalog at the root\n// these urls can be constants or calculated from existing urls\nconst colorUrl =  combine(\"/\", \"sites/dev\", \"_catalogs/theme/15/palette011.spcolor\");\n// this gives us the same result\nconst fontUrl = \"/sites/dev/_catalogs/theme/15/fontscheme007.spfont\";\n\n// apply the font and color, no background image, and don't share this theme\nawait web.applyTheme(colorUrl, fontUrl, \"\", false);\n
"},{"location":"sp/webs/#applywebtemplate-availablewebtemplates","title":"applyWebTemplate & availableWebTemplates","text":"

Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\nconst templates = (await web.availableWebTemplates().select(\"Name\")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name));\n\n// apply the wiki template\nconst template = templates.length > 0 ? templates[0].Name : \"STS#0\";\n\nawait web.applyWebTemplate(template);\n
"},{"location":"sp/webs/#getchanges","title":"getChanges","text":"

Returns the collection of changes from the change log that have occurred within the web, based on the specified query.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\n\nconst sp = spfi(...);\nconst web = sp.web;\n// get the web changes including add, update, and delete\nconst changes = await web.getChanges({\n        Add: true,\n        ChangeTokenEnd: undefined,\n        ChangeTokenStart: undefined,\n        DeleteObject: true,\n        Update: true,\n        Web: true,\n    });\n
"},{"location":"sp/webs/#maptoicon","title":"mapToIcon","text":"

Returns the name of the image file for the icon that is used to represent the specified file

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { combine } from \"@pnp/core\";\n\nconst iconFileName = await web.mapToIcon(\"test.docx\");\n// iconPath === \"icdocx.png\"\n// which you can need to map to a real url\nconst iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`;\n\n// OR dynamically\nconst sp = spfi(...);\nconst webData = await sp.web.select(\"Url\")();\nconst iconFullPath2 = combine(webData.Url, \"_layouts\", \"images\", iconFileName);\n\n// OR within SPFx using the context\nconst iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, \"_layouts\", \"images\", iconFileName);\n\n// You can also set size\n// 16x16 pixels = 0, 32x32 pixels = 1\nconst icon32FileName = await web.mapToIcon(\"test.docx\", 1);\n
"},{"location":"sp/webs/#storage-entities","title":"storage entities","text":"
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/appcatalog\";\nimport { IStorageEntity } from \"@pnp/sp/webs\";\n\n// needs to be unique, GUIDs are great\nconst key = \"my-storage-key\";\n\nconst sp = spfi(...);\n\n// read an existing entity\nconst entity: IStorageEntity = await sp.web.getStorageEntity(key);\n\n// setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site\n// you can get the tenant app catalog using the getTenantAppCatalogWeb\nconst tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb();\n\ntenantAppCatalogWeb.setStorageEntity(key, \"new value\");\n\n// set other properties\ntenantAppCatalogWeb.setStorageEntity(key, \"another value\", \"description\", \"comments\");\n\nconst entity2: IStorageEntity = await sp.web.getStorageEntity(key);\n/*\nentity2 === {\n    Value: \"another value\",\n    Comment: \"comments\";\n    Description: \"description\",\n};\n*/\n\n// you can also remove a storage entity\nawait tenantAppCatalogWeb.removeStorageEntity(key);\n
"},{"location":"sp/webs/#getappcatalog","title":"getAppCatalog","text":"

Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IApp } from \"@pnp/sp/appcatalog\";\n\nconst sp = spfi(...);\n\nconst appWeb = sp.web.appcatalog;\nconst app: IApp = appWeb.getAppById(\"{your app id}\");\n// appWeb url === web url\n
"},{"location":"sp/webs/#client-side-pages","title":"client-side-pages","text":"

You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/clientside-pages/web\";\n\nconst sp = spfi(...);\n\n// simplest add a page example\nconst page = await sp.web.addClientsidePage(\"mypage1\");\n\n// simplest load a page example\nconst page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");\n
"},{"location":"sp/webs/#contenttypes","title":"contentTypes","text":"

Allows access to the collection of content types in this web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/content-types/web\";\n\nconst sp = spfi(...);\n\nconst cts = await sp.web.contentTypes();\n\n// you can also select fields and use other odata operators\nconst cts2 = await sp.web.contentTypes.select(\"Name\")();\n
"},{"location":"sp/webs/#features","title":"features","text":"

Allows access to the collection of content types in this web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/features/web\";\n\nconst sp = spfi(...);\n\nconst features = await sp.web.features();\n
"},{"location":"sp/webs/#fields","title":"fields","text":"

Allows access to the collection of fields in this web.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/fields/web\";\n\nconst sp = spfi(...);\nconst fields = await sp.web.fields();\n
"},{"location":"sp/webs/#getfilebyserverrelativepath","title":"getFileByServerRelativePath","text":"

Gets a file by server relative url if your file name contains # and % characters

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/files/web\";\nimport { IFile } from \"@pnp/sp/files/types\";\n\nconst sp = spfi(...);\nconst file: IFile = web.getFileByServerRelativePath(\"/sites/dev/library/my # file%.docx\");\n
"},{"location":"sp/webs/#folders","title":"folders","text":"

Gets the collection of folders in this web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\n\nconst sp = spfi(...);\n\nconst folders = await sp.web.folders();\n\n// you can also filter and select as with any collection\nconst folders2 = await sp.web.folders.select(\"ServerRelativeUrl\", \"TimeLastModified\").filter(\"ItemCount gt 0\")();\n\n// or get the most recently modified folder\nconst folders2 = await sp.web.folders.orderBy(\"TimeLastModified\").top(1)();\n
"},{"location":"sp/webs/#rootfolder","title":"rootFolder","text":"

Gets the root folder of the web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\n\nconst sp = spfi(...);\n\nconst folder = await sp.web.rootFolder();\n
"},{"location":"sp/webs/#getfolderbyserverrelativepath","title":"getFolderByServerRelativePath","text":"

Gets a folder by server relative url if your folder name contains # and % characters

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/folders/web\";\nimport { IFolder } from \"@pnp/sp/folders\";\n\nconst sp = spfi(...);\n\nconst folder: IFolder = web.getFolderByServerRelativePath(\"/sites/dev/library/my # folder%/\");\n
"},{"location":"sp/webs/#hubsitedata","title":"hubSiteData","text":"

Gets hub site data for the current web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\n// get the data and force a refresh\nconst data = await sp.web.hubSiteData(true);\n
"},{"location":"sp/webs/#synchubsitetheme","title":"syncHubSiteTheme","text":"

Applies theme updates from the parent hub site collection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/hubsites/web\";\n\nconst sp = spfi(...);\nawait sp.web.syncHubSiteTheme();\n
"},{"location":"sp/webs/#lists","title":"lists","text":"

Gets the collection of all lists that are contained in the Web site

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { ILists } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst lists: ILists = sp.web.lists;\n\n// you can always order the lists and select properties\nconst data = await lists.select(\"Title\").orderBy(\"Title\")();\n\n// and use other odata operators as well\nconst data2 = await sp.web.lists.top(3).orderBy(\"LastItemModifiedDate\")();\n
"},{"location":"sp/webs/#siteuserinfolist","title":"siteUserInfoList","text":"

Gets the UserInfo list of the site collection that contains the Web site

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { IList } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst list: IList = sp.web.siteUserInfoList;\n\nconst data = await list();\n\n// or chain off that list to get additional details\nconst items = await list.items.top(2)();\n
"},{"location":"sp/webs/#defaultdocumentlibrary","title":"defaultDocumentLibrary","text":"

Get a reference to the default document library of a web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists/web\";\n\nconst sp = spfi(...);\nconst list: IList = sp.web.defaultDocumentLibrary;\n
"},{"location":"sp/webs/#customlisttemplates","title":"customListTemplates","text":"

Gets the collection of all list definitions and list templates that are available

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/lists/web\";\nimport { IList } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst templates = await sp.web.customListTemplates();\n\n// odata operators chain off the collection as expected\nconst templates2 = await sp.web.customListTemplates.select(\"Title\")();\n
"},{"location":"sp/webs/#getlist","title":"getList","text":"

Gets a list by server relative url (list's root folder)

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists/web\";\n\nconst sp = spfi(...);\nconst list: IList = sp.web.getList(\"/sites/dev/lists/test\");\n\nconst listData = await list();\n
"},{"location":"sp/webs/#getcatalog","title":"getCatalog","text":"

Returns the list gallery on the site

Name Value WebTemplateCatalog 111 WebPartCatalog 113 ListTemplateCatalog 114 MasterPageCatalog 116 SolutionCatalog 121 ThemeCatalog 123 DesignCatalog 124 AppDataCatalog 125
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport { IList } from \"@pnp/sp/lists\";\n\nconst sp = spfi(...);\nconst templateCatalog: IList = await sp.web.getCatalog(111);\n\nconst themeCatalog: IList = await sp.web.getCatalog(123);\n
"},{"location":"sp/webs/#navigation","title":"navigation","text":"

Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation/web\";\nimport { INavigation } from \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\nconst nav: INavigation = sp.web.navigation;\n
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/navigation/web\";\nimport { IRegionalSettings } from \"@pnp/sp/navigation\";\n\nconst sp = spfi(...);\nconst settings: IRegionalSettings = sp.web.regionalSettings;\n\nconst settingsData = await settings();\n
import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/related-items/web\";\nimport { IRelatedItemManager, IRelatedItem } from \"@pnp/sp/related-items\";\n\nconst sp = spfi(...);\nconst manager: IRelatedItemManager = sp.web.relatedItems;\n\nconst data: IRelatedItem[] = await manager.getRelatedItems(\"{list name}\", 4);\n
"},{"location":"sp/webs/#security-imports","title":"security imports","text":"

Please see information around the available security methods in the security article.

"},{"location":"sp/webs/#sharing-imports","title":"sharing imports","text":"

Please see information around the available sharing methods in the sharing article.

"},{"location":"sp/webs/#sitegroups","title":"siteGroups","text":"

The site groups

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\nconst groups = await sp.web.siteGroups();\n\nconst groups2 = await sp.web.siteGroups.top(2)();\n
"},{"location":"sp/webs/#associatedownergroup","title":"associatedOwnerGroup","text":"

The web's owner group

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nconst group = await sp.web.associatedOwnerGroup();\n\nconst users = await sp.web.associatedOwnerGroup.users();\n
"},{"location":"sp/webs/#associatedmembergroup","title":"associatedMemberGroup","text":"

The web's member group

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nconst group = await sp.web.associatedMemberGroup();\n\nconst users = await sp.web.associatedMemberGroup.users();\n
"},{"location":"sp/webs/#associatedvisitorgroup","title":"associatedVisitorGroup","text":"

The web's visitor group

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nconst group = await sp.web.associatedVisitorGroup();\n\nconst users = await sp.web.associatedVisitorGroup.users();\n
"},{"location":"sp/webs/#createdefaultassociatedgroups","title":"createDefaultAssociatedGroups","text":"

Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-groups/web\";\n\nconst sp = spfi(...);\n\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\");\n\n// copy the role assignments\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", true);\n\n// don't clear sub assignments\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, false);\n\n// specify secondary owner, don't copy permissions, clear sub scopes\nawait sp.web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, true, \"{second owner login}\");\n
"},{"location":"sp/webs/#siteusers","title":"siteUsers","text":"

The site users

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst users = await sp.web.siteUsers();\n\nconst users2 = await sp.web.siteUsers.top(5)();\n\nconst users3 = await sp.web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent(\"i:0#.f|m\")}')`)();\n
"},{"location":"sp/webs/#currentuser","title":"currentUser","text":"

Information on the current user

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\n\nconst sp = spfi(...);\n\nconst user = await sp.web.currentUser();\n\n// check the login name of the current user\nconst user2 = await sp.web.currentUser.select(\"LoginName\")();\n
"},{"location":"sp/webs/#ensureuser","title":"ensureUser","text":"

Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { IWebEnsureUserResult } from \"@pnp/sp/site-users/\";\n\nconst sp = spfi(...);\n\nconst result: IWebEnsureUserResult = await sp.web.ensureUser(\"i:0#.f|membership|user@domain.onmicrosoft.com\");\n
"},{"location":"sp/webs/#getuserbyid","title":"getUserById","text":"

Returns the user corresponding to the specified member identifier for the current web

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/site-users/web\";\nimport { ISiteUser } from \"@pnp/sp/site-users/\";\n\nconst sp = spfi(...);\n\nconst user: ISiteUser = sp.web.getUserById(23);\n\nconst userData = await user();\n\nconst userData2 = await user.select(\"LoginName\")();\n
"},{"location":"sp/webs/#usercustomactions","title":"userCustomActions","text":"

Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp/webs\";\nimport \"@pnp/sp/user-custom-actions/web\";\nimport { IUserCustomActions } from \"@pnp/sp/user-custom-actions\";\n\nconst sp = spfi(...);\n\nconst actions: IUserCustomActions = sp.web.userCustomActions;\n\nconst actionsData = await actions();\n
"},{"location":"sp/webs/#iwebinfosdata","title":"IWebInfosData","text":"

Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations.

interface IWebInfosData {\n    Configuration: number;\n    Created: string;\n    Description: string;\n    Id: string;\n    Language: number;\n    LastItemModifiedDate: string;\n    LastItemUserModifiedDate: string;\n    ServerRelativeUrl: string;\n    Title: string;\n    WebTemplate: string;\n    WebTemplateId: number;\n}\n
"},{"location":"sp-admin/","title":"sp-admin","text":"

The @pnp/sp-admin library enables you to call the static SharePoint admin API's:

  • _api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant
  • _api/Microsoft.Online.SharePoint.TenantAdministration.SiteProperties
  • _api/Microsoft.Online.SharePoint.TenantAdministration.Tenant

These APIs typically require an elevated level of permissions and should not be relied upon in general user facing solutions. Before using this library please understand the impact of what you are doing as you are updating settings at the tenant level for all users.

Warning

These APIs are officially not documented which means there is no SLA provided by Microsoft. Furthermore, they can be updated without notification.

"},{"location":"sp-admin/#use","title":"Use","text":"

To use the library you first install the package:

npm install @pnp/sp-admin --save\n

Then import the package into your solution, it will attach a node to the sp fluent interface using selective imports.

import \"@pnp/sp-admin\";\n
"},{"location":"sp-admin/#basic-example","title":"Basic Example","text":"

In this example we get all of the web templates' information.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(...);\n\n// note the \"admin\" node now available\nconst templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();\n
"},{"location":"sp-admin/#tenant","title":"tenant","text":"

The tenant node represents calls to the _api/Microsoft.Online.SharePoint.TenantAdministration.Tenant api.

When calling the tenant endpoint you must target the -admin site as shown here. If you do not you will get only errors.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(\"https://{tenant}-admin.sharepoint.com\");\n\n// The MSAL scope will be: \"https://{tenant}-admin.sharepoint.com/.default\"\n\n// default props\nconst defaultProps = await sp.admin.tenant();\n\n// all props\nconst allProps = await sp.admin.tenant.select(\"*\")();\n\n// select specific props\nconst selectedProps = await sp.admin.tenant.select(\"AllowEditing\", \"DefaultContentCenterSite\")();\n\n// call method\nconst templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();\n
"},{"location":"sp-admin/#office365tenant","title":"office365Tenant","text":"

The office365Tenant node represents calls to the _api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant end point and is accessible from any site url.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(...);\n\n// default props\nconst defaultProps = await sp.admin.office365Tenant();\n\n// all props\nconst allProps = await sp.admin.office365Tenant.select(\"*\")();\n\n// selected props\nconst selectedProps = await sp.admin.office365Tenant.select(\"AllowEditing\", \"DefaultContentCenterSite\")();\n\n// call method\nconst externalUsers = await sp.admin.office365Tenant.getExternalUsers();\n
"},{"location":"sp-admin/#siteproperties","title":"siteProperties","text":"

The siteProperties node is primarily for accessing detailed properties about a site and tenant.

import { spfi } from \"@pnp/sp\";\nimport \"@pnp/sp-admin\";\n\nconst sp = spfi(...);\n\n// default props\nconst defaultProps = await sp.admin.siteProperties();\n\n// all props\nconst allProps = await sp.admin.siteProperties.select(\"*\")();\n\n// selected props\nconst selectedProps = await sp.admin.siteProperties.select(\"LockState\")();\n\n// call method\nawait sp.admin.siteProperties.clearSharingLockDown(\"https://tenant.sharepoint.com/sites/site1\");\n

For more information on the methods available and how to use them, please review the code comments in the source.

"},{"location":"sp-admin/#call","title":"call","text":"

All those nodes support a call method to easily allow calling methods not explictly added to the library. If there is a method you use often that would be a good candidate to add, please open an issue or submit a PR. The call method is meant to help unblock folks before methods are added.

This sample shows using call to invoke the \"AddTenantCdnOrigin\" method of office365Tenant. While we already support for this method, it helps to show the relationship between call and an existing method.

import { spfi } from \"@pnp/sp\";\nimport { SPOTenantCdnType } from '@pnp/sp-admin';\n\nconst sp = spfi(...);\n\n// call AddTenantCdnOrigin\nawait sp.admin.office365Tenant.call<void>(\"AddTenantCdnOrigin\", {\n    \"cdnType\": SPOTenantCdnType.Public,\n    \"originUrl\": \"*/clientsideassets\"\n});\n\nconst spTenant = spfi(\"https://{tenant}-admin.sharepoint.com\");\n\n// call GetSiteSubscriptionId which takes no args\nconst id = await spTenant.admin.tenant.call<string>(\"GetSiteSubscriptionId\");\n
"}]} \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 000000000..76991bd4a --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,573 @@ + + + + https://pnp.github.io/pnpjs/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/getting-started/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/packages/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/transition-guide/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/azidjsclient/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/adv-clientside-pages/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/auth-browser/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/auth-nodejs/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/auth-spfx/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/authentication/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/batching-caching/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/batching/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/calling-other-endpoints/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/custom-bundle/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/error-handling/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/invokable/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/nightly-builds/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/project-preset/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/selective-imports/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/concepts/typings/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/debug-tests/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/debugging/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/documentation/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/extending-the-library/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/local-debug-configuration/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/npm-scripts/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/pull-requests/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/settings/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/contributing/setup-dev-machine/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/behavior-recipes/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/behaviors/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/moments/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/observers/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/storage/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/timeline/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/core/util/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/behaviors/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/bookings/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/calendars/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/cloud-communications/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/columns/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/contacts/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/content-types/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/directoryobjects/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/groups/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/insights/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/invitations/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/items/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/lists/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/messages/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/onedrive/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/outlook/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/photos/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/planner/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/search/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/shares/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/sites/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/subscriptions/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/teams/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/graph/users/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/logging/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/msaljsclient/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/news/2020-year-in-review/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/news/2021-year-in-review/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/news/2022-year-in-review/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/nodejs/behaviors/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/nodejs/sp-extensions/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/queryable/behaviors/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/queryable/extensions/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/queryable/queryable/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/alias-parameters/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/alm/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/attachments/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/behaviors/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/clientside-pages/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/column-defaults/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/comments-likes/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/content-types/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/context-info/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/favorites/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/features/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/fields/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/files/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/folders/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/forms/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/groupSiteManager/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/hubsites/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/items/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/lists/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/navigation/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/permissions/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/profiles/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/publishing-sitepageservice/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/recycle-bin/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/regional-settings/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/related-items/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/search/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/security/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/sharing/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/site-designs/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/site-groups/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/site-scripts/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/site-users/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/sites/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/social/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/sp-utilities-utility/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/subscriptions/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/taxonomy/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/tenant-properties/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/user-custom-actions/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/views/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp/webs/ + 2023-12-11 + daily + + + https://pnp.github.io/pnpjs/sp-admin/ + 2023-12-11 + daily + + \ No newline at end of file diff --git a/sitemap.xml.gz b/sitemap.xml.gz new file mode 100644 index 000000000..30606b0b7 Binary files /dev/null and b/sitemap.xml.gz differ diff --git a/sp-admin/index.html b/sp-admin/index.html new file mode 100644 index 000000000..cc3930a40 --- /dev/null +++ b/sp-admin/index.html @@ -0,0 +1,2844 @@ + + + + + + + + + + + + + + + + + + + + + + + + sp-admin - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

sp-admin

+

The @pnp/sp-admin library enables you to call the static SharePoint admin API's:

+
    +
  • _api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant
  • +
  • _api/Microsoft.Online.SharePoint.TenantAdministration.SiteProperties
  • +
  • _api/Microsoft.Online.SharePoint.TenantAdministration.Tenant
  • +
+

These APIs typically require an elevated level of permissions and should not be relied upon in general user facing solutions. Before using this library please understand the impact of what you are doing as you are updating settings at the tenant level for all users.

+
+

Warning

+

These APIs are officially not documented which means there is no SLA provided by Microsoft. Furthermore, they can be updated without notification.

+
+

Use

+

To use the library you first install the package:

+
npm install @pnp/sp-admin --save
+
+

Then import the package into your solution, it will attach a node to the sp fluent interface using selective imports.

+
import "@pnp/sp-admin";
+
+

Basic Example

+

In this example we get all of the web templates' information.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi(...);
+
+// note the "admin" node now available
+const templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();
+
+

tenant

+

The tenant node represents calls to the _api/Microsoft.Online.SharePoint.TenantAdministration.Tenant api.

+
+

When calling the tenant endpoint you must target the -admin site as shown here. If you do not you will get only errors.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi("https://{tenant}-admin.sharepoint.com");
+
+// The MSAL scope will be: "https://{tenant}-admin.sharepoint.com/.default"
+
+// default props
+const defaultProps = await sp.admin.tenant();
+
+// all props
+const allProps = await sp.admin.tenant.select("*")();
+
+// select specific props
+const selectedProps = await sp.admin.tenant.select("AllowEditing", "DefaultContentCenterSite")();
+
+// call method
+const templates = await sp.admin.tenant.getSPOTenantAllWebTemplates();
+
+

office365Tenant

+

The office365Tenant node represents calls to the _api/Microsoft.Online.SharePoint.TenantManagement.Office365Tenant end point and is accessible from any site url.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi(...);
+
+// default props
+const defaultProps = await sp.admin.office365Tenant();
+
+// all props
+const allProps = await sp.admin.office365Tenant.select("*")();
+
+// selected props
+const selectedProps = await sp.admin.office365Tenant.select("AllowEditing", "DefaultContentCenterSite")();
+
+// call method
+const externalUsers = await sp.admin.office365Tenant.getExternalUsers();
+
+

siteProperties

+

The siteProperties node is primarily for accessing detailed properties about a site and tenant.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp-admin";
+
+const sp = spfi(...);
+
+// default props
+const defaultProps = await sp.admin.siteProperties();
+
+// all props
+const allProps = await sp.admin.siteProperties.select("*")();
+
+// selected props
+const selectedProps = await sp.admin.siteProperties.select("LockState")();
+
+// call method
+await sp.admin.siteProperties.clearSharingLockDown("https://tenant.sharepoint.com/sites/site1");
+
+
+

For more information on the methods available and how to use them, please review the code comments in the source.

+
+

call

+

All those nodes support a call method to easily allow calling methods not explictly added to the library. If there is a method you use often that would be a good candidate to add, please open an issue or submit a PR. The call method is meant to help unblock folks before methods are added.

+

This sample shows using call to invoke the "AddTenantCdnOrigin" method of office365Tenant. While we already support for this method, it helps to show the relationship between call and an existing method.

+
import { spfi } from "@pnp/sp";
+import { SPOTenantCdnType } from '@pnp/sp-admin';
+
+const sp = spfi(...);
+
+// call AddTenantCdnOrigin
+await sp.admin.office365Tenant.call<void>("AddTenantCdnOrigin", {
+    "cdnType": SPOTenantCdnType.Public,
+    "originUrl": "*/clientsideassets"
+});
+
+const spTenant = spfi("https://{tenant}-admin.sharepoint.com");
+
+// call GetSiteSubscriptionId which takes no args
+const id = await spTenant.admin.tenant.call<string>("GetSiteSubscriptionId");
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/alias-parameters/index.html b/sp/alias-parameters/index.html new file mode 100644 index 000000000..1a32c62f8 --- /dev/null +++ b/sp/alias-parameters/index.html @@ -0,0 +1,2776 @@ + + + + + + + + + + + + + + + + + + + + + + + + alias parameters - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp - Aliased Parameters

+

Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders.

+

To alias a parameter you include the label name, a separator ("::") and the value in the string. You also need to prepend a "!" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a "@" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use "@p1" you should use "@p2" for a second parameter alias in the same query.

+

Construct a parameter alias

+

Pattern: !@{label name}::{value}

+

Example: "!@p1::\sites\dev" or "!@p2::\text.txt"

+

Example without aliasing

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+const sp = spfi(...);
+
+// still works as expected, no aliasing
+const query = sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files
+console.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3
+
+const r = await query();
+console.log(r);
+
+

Example with aliasing

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// same query with aliasing
+const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
+console.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
+
+const r = await query();
+console.log(r);
+
+

Example with aliasing and batching

+

Aliasing is supported with batching as well:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// same query with aliasing and batching
+const [batchedWeb, execute] = await sp.web.batched();
+
+const query = batchedWeb.web.getFolderByServerRelativePath("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
+console.log(query.toRequestUrl()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
+
+query().then(r => {
+
+    console.log(r);
+});
+
+execute();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/alm/index.html b/sp/alm/index.html new file mode 100644 index 000000000..a6cd86fc4 --- /dev/null +++ b/sp/alm/index.html @@ -0,0 +1,2916 @@ + + + + + + + + + + + + + + + + + + + + + + + + ALM api - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/appcatalog

+

The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.

+

Understanding the App Catalog Hierarchy

+

Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.

+

Referencing an App Catalog

+

There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.

+

Get tenant app catalog

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// get the current context web's app catalog
+// this will be the site collection app catalog
+const availableApps = await sp.tenantAppcatalog();
+
+

Get site collection AppCatalog

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// get the current context web's app catalog
+const availableApps = await sp.web.appcatalog();
+
+

Get site collection AppCatalog by URL

+

If you know the url of the site collection whose app catalog you want you can use the following code. First you need to use one of the methods to access a web. Once you have the web instance you can call the .appcatalog property on that web instance.

+
+

If a given site collection does not have an app catalog trying to access it will throw an error.

+
+
import { spfi } from "@pnp/sp";
+import { Web } from '@pnp/sp/webs';
+
+const sp = spfi(...);
+const web = Web([sp.web, "https://mytenant.sharepoint.com/sites/mysite"]);
+const catalog = await web.appcatalog();
+
+

The following examples make use of a variable "catalog" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.

+

List Available Apps

+

The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.

+
// get available apps
+await catalog();
+
+// get available apps selecting two fields
+await catalog.select("Title", "Deployed")();
+
+

Add an App

+

This action must be performed in the context of the tenant app catalog

+

Batching Not Supported Banner

+
// this represents the file bytes of the app package file
+const blob = new Blob();
+
+// there is an optional third argument to control overwriting existing files
+const r = await catalog.add("myapp.app", blob);
+
+// this is at its core a file add operation so you have access to the response data as well
+// as a File instance representing the created file
+console.log(JSON.stringify(r.data, null, 4));
+
+// all file operations are available
+const nameData = await r.file.select("Name")();
+
+

Get an App

+

You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions

+
const app = await catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c")();
+
+

Perform app actions

+

Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block.

+
const myAppId = "5137dff1-0b79-4ebc-8af4-ca01f7bd393c";
+
+// deploy
+await catalog.getAppById(myAppId).deploy();
+
+// retract
+await catalog.getAppById(myAppId).retract();
+
+// install
+await catalog.getAppById(myAppId).install();
+
+// uninstall
+await catalog.getAppById(myAppId).uninstall();
+
+// upgrade
+await catalog.getAppById(myAppId).upgrade();
+
+// remove
+await catalog.getAppById(myAppId).remove();
+
+
+

Synchronize a solution/app to the Microsoft Teams App Catalog

+

By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id.

+
// Using the app id
+await catalog.syncSolutionToTeams("5137dff1-0b79-4ebc-8af4-ca01f7bd393c");
+
+// Using the SharePoint apps item id
+await catalog.syncSolutionToTeams("123", true);
+
+

Notes

+
    +
  • The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.
  • +
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/attachments/index.html b/sp/attachments/index.html new file mode 100644 index 000000000..1e2873e18 --- /dev/null +++ b/sp/attachments/index.html @@ -0,0 +1,2874 @@ + + + + + + + + + + + + + + + + + + + + + + + + attachments - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/attachments

+

The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.

+

Get attachments

+
import { spfi } from "@pnp/sp";
+import { IAttachmentInfo } from "@pnp/sp/attachments";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+// get all the attachments
+const info: IAttachmentInfo[] = await item.attachmentFiles();
+
+// get a single file by file name
+const info2: IAttachmentInfo = await item.attachmentFiles.getByName("file.txt")();
+
+// select specific properties using odata operators and use Pick to type the result
+const info3: Pick<IAttachmentInfo, "ServerRelativeUrl">[] = await item.attachmentFiles.select("ServerRelativeUrl")();
+
+

Add an Attachment

+

You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.

+

Batching Not Supported Banner

+
import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.add("file2.txt", "Here is my content");
+
+

Read Attachment Content

+

You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.

+
import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+const text = await item.attachmentFiles.getByName("file.txt").getText();
+
+// use this in the browser, does not work in nodejs
+const blob = await item.attachmentFiles.getByName("file.mp4").getBlob();
+
+// use this in nodejs
+const buffer = await item.attachmentFiles.getByName("file.mp4").getBuffer();
+
+// file must be valid json
+const json = await item.attachmentFiles.getByName("file.json").getJSON();
+
+

Update Attachment Content

+

You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. +Batching Not Supported Banner

+
import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.getByName("file2.txt").setContent("My new content!!!");
+
+

Delete Attachment

+
import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.getByName("file2.txt").delete();
+
+

Recycle Attachment

+

Delete the attachment and send it to recycle bin

+
import { spfi } from "@pnp/sp";
+import { IItem } from "@pnp/sp/items/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+await item.attachmentFiles.getByName("file2.txt").recycle();
+
+

Recycle Multiple Attachments

+

Delete multiple attachments and send them to recycle bin

+
import { spfi } from "@pnp/sp";
+import { IList } from "@pnp/sp/lists/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items";
+import "@pnp/sp/attachments";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+const item = await batchedSP.web.lists.getByTitle("MyList").items.getById(2);
+
+item.attachmentFiles.getByName("1.txt").recycle();
+item.attachmentFiles.getByName("2.txt").recycle();
+
+await execute();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/behaviors/index.html b/sp/behaviors/index.html new file mode 100644 index 000000000..1c6ca7f1f --- /dev/null +++ b/sp/behaviors/index.html @@ -0,0 +1,2851 @@ + + + + + + + + + + + + + + + + + + + + + + + + behaviors - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp : behaviors

+

The article describes the behaviors exported by the @pnp/sp library. Please also see available behaviors in @pnp/core, @pnp/queryable, @pnp/graph, and @pnp/nodejs.

+

DefaultInit

+

The DefaultInit behavior, is a composed behavior which includes Telemetry, RejectOnError, and ResolveOnData. Additionally, it sets the cache and credentials properties of the RequestInit.

+
import { spfi, DefaultInit } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(DefaultInit());
+
+await sp.web();
+
+

DefaultHeaders

+

The DefaultHeaders behavior uses InjectHeaders to set the Accept, Content-Type, and User-Agent headers.

+
import { spfi, DefaultHeaders } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(DefaultHeaders());
+
+await sp.web();
+
+
+

DefaultInit and DefaultHeaders are separated to make it easier to create your own default headers or init behavior. You should include both if composing your own default behavior.

+
+

RequestDigest

+

The RequestDigest behavior ensures that the "X-RequestDigest" header is included for requests where it is needed. If you are using MSAL, supplying your own tokens, or doing a GET request it is not required. As well it cache's the digests to reduce the number of requests.

+

Optionally you can provide a function to supply your own digests. The logic followed by the behavior is to check the cache, run a hook if provided, and finally make a request to "/_api/contextinfo" for the value.

+
import { spfi, RequestDigest } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(RequestDigest());
+
+await sp.web();
+
+

With a hook:

+
import { dateAdd } from "@pnp/core";
+import { spfi, RequestDigest } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(RequestDigest((url, init) => {
+
+    // the url will be a URL instance representing the request url
+    // init will be the RequestInit
+
+    return {
+        expiration: dateAdd(new Date(), "minute", 20);
+        value: "MY VALID REQUEST DIGEST VALUE";
+    }
+}));
+
+await sp.web();
+
+

SPBrowser

+

A composed behavior suitable for use within a SPA or other scenario outside of SPFx. It includes DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. As well it adds a pre observer to try and ensure the request url is absolute if one is supplied in props.

+

The baseUrl prop can be used to configure a fallback when making urls absolute.

+
+

If you are building a SPA you likely need to handle authentication. For this we support the msal library which you can use directly or as a pattern to roll your own MSAL implementation behavior.

+
+

You should set a baseUrl as shown below.

+
import { spfi, SPBrowser } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// you should use the baseUrl value when working in a SPA to ensure it is always properly set for all requests
+const sp = spfi().using(SPBrowser({ baseUrl: "https://tenant.sharepoint.com/sites/dev" }));
+
+await sp.web();
+
+

SPFx

+

This behavior is designed to work closely with SPFx. The only parameter is the current SPFx Context. SPFx is a composed behavior including DefaultHeaders, DefaultInit, BrowserFetchWithRetry, DefaultParse, and RequestDigest. A hook is supplied to RequestDigest that will attempt to use any existing legacyPageContext formDigestValue it can find, otherwise defaults to the base RequestDigest behavior. It also sets a pre handler to ensure the url is absolute, using the SPFx context's pageContext.web.absoluteUrl as the base.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const sp = spfi(...).using(SPFx(this.context));
+
+await sp.web();
+
+

Note that both the sp and graph libraries export an SPFx behavior. They are unique to their respective libraries and cannot be shared, i.e. you can't use the graph SPFx to setup sp and vice-versa.

+
import { GraphFI, graphfi, SPFx as graphSPFx } from '@pnp/graph'
+import { SPFI, spfi, SPFx as spSPFx } from '@pnp/sp'
+
+const sp = spfi().using(spSPFx(this.context));
+const graph = graphfi().using(graphSPFx(this.context));
+
+

SPFxToken

+

Added in 3.12

+

Allows you to include the SharePoint Framework application token in requests. This behavior is include within the SPFx behavior, but is available separately should you wish to compose it into your own behaviors.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+// this.context represents the context object within an SPFx webpart, application customizer, or ACE.
+const sp = spfi(...).using(SPFxToken(this.context));
+
+await sp.web();
+
+

Telemetry

+

This behavior helps provide usage statistics to us about the number of requests made to the service using this library, as well as the methods being called. We do not, and cannot, access any PII information or tie requests to specific users. The data aggregates at the tenant level. We use this information to better understand how the library is being used and look for opportunities to improve high-use code paths.

+
+

You can always opt out of the telemetry by creating your own default behaviors and leaving it out. However, we encourgage you to include it as it helps us understand usage and impact of the work.

+
+
import { spfi, Telemetry } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi().using(Telemetry());
+
+await sp.web();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/clientside-pages/index.html b/sp/clientside-pages/index.html new file mode 100644 index 000000000..31b4c1538 --- /dev/null +++ b/sp/clientside-pages/index.html @@ -0,0 +1,4242 @@ + + + + + + + + + + + + + + + + + + + + + + + + client-side pages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/clientside-pages

+

The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts.

+

Selective Imports Banner

+

Create a new Page

+

You can create a new client-side page in several ways, all are equivalent.

+

Create using IWeb.addClientsidePage

+
import { spfi, SPFI } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+import { PromotedState } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// Create a page providing a file name
+const page = await sp.web.addClientsidePage("mypage1");
+
+// ... other operations on the page as outlined below
+
+// the page is initially not published, you must publish it so it appears for others users
+await page.save();
+
+// include title and page layout
+const page2 = await sp.web.addClientsidePage("mypage", "My Page Title", "Article");
+
+// you must publish the new page
+await page2.save();
+
+// include title, page layout, and specifying the publishing status (Added in 2.0.4)
+const page3 = await sp.web.addClientsidePage("mypage", "My Page Title", "Article", PromotedState.PromoteOnPublish);
+
+// you must publish the new page, after which the page will immediately be promoted to a news article
+await page3.save();
+
+

Create using CreateClientsidePage method

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { Web } from "@pnp/sp/webs";
+import { CreateClientsidePage, PromotedState } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page1 = await CreateClientsidePage(sp.web, "mypage2", "My Page Title");
+
+// you must publish the new page
+await page1.save(true);
+
+// specify the page layout type parameter
+const page2 = await CreateClientsidePage(sp.web, "mypage3", "My Page Title", "Article");
+
+// you must publish the new page
+await page2.save();
+
+// specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4)
+const page2half = await CreateClientsidePage(sp.web, "mypage3", "My Page Title", "Article", PromotedState.PromoteOnPublish);
+
+// you must publish the new page, after which the page will immediately be promoted to a news article
+await page2half.save();
+
+// use the web factory to create a page in a specific web
+const page3 = await CreateClientsidePage(Web([sp, "https://{absolute web url}"]), "mypage4", "My Page Title");
+
+// you must publish the new page
+await page3.save();
+
+

Create using IWeb.addFullPageApp

+

Using this method you can easily create a full page app page given the component id. Don't forget the page will not be published and you will need to call save.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page = await sp.web.addFullPageApp("name333", "My Title", "2CE4E250-B997-11EB-A9D2-C9D2FF95D000");
+// ... other page actions
+// you must save the page to publish it
+await page.save();
+
+

Load Pages

+

There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.

+

Load using IWeb.loadClientsidePage

+

This method takes a server relative path to the page to load.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { Web } from "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+
+const sp = spfi(...);
+
+// use from the sp.web fluent chain
+const page = await sp.web.loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
+
+// use the web factory to target a specific web
+const page2 = await Web([sp.web, "https://{absolute web url}"]).loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
+
+

Load using ClientsidePageFromFile

+

This method takes an IFile instance and loads an IClientsidePage instance.

+
import { spfi } from "@pnp/sp";
+import { ClientsidePageFromFile } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+
+const sp = spfi(...);
+
+const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath("/sites/dev/sitepages/mypage3.aspx"));
+
+

Edit Sections and Columns

+

Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12
+const section1 = page.addSection();
+section1.addColumn(6);
+section1.addColumn(6);
+
+// create a three column layout in a new section
+const section2 = page.addSection();
+section2.addColumn(4);
+section2.addColumn(4);
+section2.addColumn(4);
+
+// publish our changes
+await page.save();
+
+

Manipulate Sections and Columns

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// drop all the columns in this section
+// this will also DELETE all controls contained in the columns
+page.sections[1].columns.length = 0;
+
+// create a new column layout
+page.sections[1].addColumn(4);
+page.sections[1].addColumn(8);
+
+// publish our changes
+await page.save();
+
+

Vertical Section

+

The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// add or get a vertical section (handles case where section already exists)
+const vertSection = page.addVerticalSection();
+
+// ****************************************************************
+
+// if you know or want to test if a vertical section is present:
+if (page.hasVerticalSection) {
+
+    // access the vertical section (this method will NOT create the section if it does not exist)
+    page.verticalSection.addControl(new ClientsideText("hello"));
+} else {
+
+    const vertSection = page.addVerticalSection();
+    vertSection.addControl(new ClientsideText("hello"));
+}
+
+

Reorder Sections

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// swap the order of two sections
+// this will preserve the controls within the columns
+page.sections = [page.sections[1], page.sections[0]];
+
+// publish our changes
+await page.save();
+
+

Reorder Columns

+

The sections and columns are arrays, so normal array operations work as expected

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// swap the order of two columns
+// this will preserve the controls within the columns
+page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]];
+
+// publish our changes
+await page.save();
+
+

Clientside Controls

+

Once you have your sections and columns defined you will want to add/edit controls within those columns.

+

Add Text Content

+
import { spfi } from "@pnp/sp";
+import { ClientsideText, IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+page.addSection().addControl(new ClientsideText("@pnp/sp is a great library!"));
+
+await page.save();
+
+

Add Controls

+

Adding controls involves loading the available client-side part definitions from the server or creating a text part.

+
import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+import { spfi } from "@pnp/sp";
+import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// this will be a ClientsidePageComponent array
+// this can be cached on the client in production scenarios
+const partDefs = await sp.web.getClientsideWebParts();
+
+// find the definition we want, here by id
+const partDef = partDefs.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa");
+
+// optionally ensure you found the def
+if (partDef.length < 1) {
+    // we didn't find it so we throw an error
+    throw new Error("Could not find the web part");
+}
+
+// create a ClientWebPart instance from the definition
+const part = ClientsideWebpart.fromComponentDef(partDef[0]);
+
+// set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video.
+// the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting
+// the properties correctly
+part.setProperties<{ embedCode: string }>({
+    embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg",
+});
+
+// we add that part to a new section
+page.addSection().addControl(part);
+
+await page.save();
+
+

Handle Different Webpart's Settings

+

There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title.

+
import { spfi } from "@pnp/sp";
+import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+
+// we create a class to wrap our functionality in a reusable way
+class ListWebpart extends ClientsideWebpart {
+
+  constructor(control: ClientsideWebpart) {
+    super((<any>control).json);
+  }
+
+  // add property getter/setter for what we need, in this case "listTitle" within searchablePlainTexts
+  public get DisplayTitle(): string {
+    return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || "";
+  }
+
+  public set DisplayTitle(value: string) {
+    this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value;
+  }
+}
+
+const sp = spfi(...);
+
+// now we load our page
+const page = await sp.web.loadClientsidePage("/sites/dev/SitePages/List-Web-Part.aspx");
+
+// get our part and pass it to the constructor of our wrapper class
+const part = new ListWebpart(page.sections[0].columns[0].getControl(0));
+
+part.DisplayTitle = "My New Title!";
+
+await page.save();
+
+
+

Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.

+
+

Page Operations

+

There are other operation you can perform on a page in addition to manipulating the content.

+

pageLayout

+

You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.pageLayout;
+
+// set the value
+page.pageLayout = "Article";
+await page.save();
+
+

bannerImageUrl

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.bannerImageUrl;
+
+// set the value
+page.bannerImageUrl = "/server/relative/path/to/image.png";
+await page.save();
+
+
+

Banner images need to exist within the same site collection as the page where you want to use them.

+
+

thumbnailUrl

+

Allows you to set the thumbnail used for the page independently of the banner.

+
+

If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality.

+
+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.thumbnailUrl;
+
+// set the value
+page.thumbnailUrl = "/server/relative/path/to/image.png";
+await page.save();
+
+

topicHeader

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.topicHeader;
+
+// set the value
+page.topicHeader = "My cool header!";
+await page.save();
+
+// clear the topic header and hide it
+page.topicHeader = "";
+await page.save();
+
+

title

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.title;
+
+// set the value
+page.title = "My page title";
+await page.save();
+
+

description

+
+

Descriptions are limited to 255 chars

+
+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.description;
+
+// set the value
+page.description = "A description";
+await page.save();
+
+

layoutType

+

Sets the layout type of the page. The valid values are: "FullWidthImage", "NoImage", "ColorBlock", "CutInShape"

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.layoutType;
+
+// set the value
+page.layoutType = "ColorBlock";
+await page.save();
+
+

headerTextAlignment

+

Sets the header text alignment to one of "Left" or "Center"

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.headerTextAlignment;
+
+// set the value
+page.headerTextAlignment = "Center";
+await page.save();
+
+

showTopicHeader

+

Sets if the topic header is displayed on a page.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.showTopicHeader;
+
+// show the header
+page.showTopicHeader = true;
+await page.save();
+
+// hide the header
+page.showTopicHeader = false;
+await page.save();
+
+

showPublishDate

+

Sets if the publish date is displayed on a page.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the current value
+const value = page.showPublishDate;
+
+// show the date
+page.showPublishDate = true;
+await page.save();
+
+// hide the date
+page.showPublishDate = false;
+await page.save();
+
+

Get / Set author details

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/clientside-pages";
+import "@pnp/sp/site-users";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// get the author details (string | null)
+const value = page.authorByLine;
+
+// set the author by user id
+const user = await sp.web.currentUser.select("Id", "LoginName")();
+const userId = user.Id;
+const userLogin = user.LoginName;
+
+await page.setAuthorById(userId);
+await page.save();
+
+await page.setAuthorByLoginName(userLogin);
+await page.save();
+
+
+

you must still save the page after setting the author to persist your changes as shown in the example.

+
+

load

+

Loads the page from the server. This will overwrite any local unsaved changes.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+await page.load();
+
+

save

+
+

Known Issue Banner Uncustomized home pages (i.e the home page that is generated with a site out of the box) cannot be updated by this library without becoming corrupted.

+
+

Saves any changes to the page, optionally keeping them in draft state.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// changes are published
+await page.save();
+
+// changes remain in draft
+await page.save(false);
+
+

discardPageCheckout

+

Discards any current checkout of the page by the current user.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+await page.discardPageCheckout();
+
+

schedulePublish

+

Schedules the page for publishing.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// date and time to publish the page in UTC.
+const publishDate = new Date("1/1/1901");
+
+const scheduleVersion: string = await page.schedulePublish(publishDate);
+
+

promoteToNews

+

Promotes the page as a news article.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+await page.promoteToNews();
+
+

enableComments & disableComments

+

Used to control the availability of comments on a page.

+

Known Issue Banner

+
import { spfi } from "@pnp/sp";
+// you need to import the comments sub-module or use the all preset
+import "@pnp/sp/comments/clientside-page";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// turn on comments
+await page.enableComments();
+
+// turn off comments
+await page.disableComments();
+
+

findControlById

+

Finds a control within the page by id.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage, ClientsideText } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+const control = page.findControlById("06d4cdf6-bce6-4200-8b93-667a1b0a6c9d");
+
+// you can also type the control
+const control = page.findControlById<ClientsideText>("06d4cdf6-bce6-4200-8b93-667a1b0a6c9d");
+
+

findControl

+

Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// find the first control whose order is 9
+const control = page.findControl((c) => c.order === 9);
+
+// iterate all the controls and output the id to the console
+page.findControl((c) => {
+    console.log(c.id);
+    return false;
+});
+
+

like & unlike

+

Updates the page's like value for the current user.

+
// our page instance
+const page: IClientsidePage;
+
+// like this page
+await page.like();
+
+// unlike this page
+await page.unlike();
+
+

getLikedByInformation

+

Gets the likes information for this page.

+
// our page instance
+const page: IClientsidePage;
+
+const info = await page.getLikedByInformation();
+
+

copy

+

Creates a copy of the page, including all controls.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// creates a published copy of the page
+const pageCopy = await page.copy(sp.web, "newpagename", "New Page Title");
+
+// creates a draft (unpublished) copy of the page
+const pageCopy2 = await page.copy(sp.web, "newpagename", "New Page Title", false);
+
+// edits to pageCopy2 ...
+
+// publish the page
+pageCopy2.save();
+
+

copyTo

+

Copies the contents of a page to another existing page instance.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// our page instances, loaded in any of the ways shown above
+const source: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+const target: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/target.aspx");
+const target2: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/target2.aspx");
+
+// creates a published copy of the page
+await source.copyTo(target);
+
+// creates a draft (unpublished) copy of the page
+await source.copyTo(target2, false);
+
+// edits to target2...
+
+// publish the page
+target2.save();
+
+

setBannerImage

+

Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent.

+
+

Banner images need to exist within the same site collection as the page where you want to use them.

+
+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+page.setBannerImage("/server/relative/path/to/image.png");
+
+// save the changes
+await page.save();
+
+// set additional props
+page.setBannerImage("/server/relative/path/to/image.png", {
+  altText: "Image description",
+  imageSourceType: 2,
+  translateX: 30,
+  translateY: 1234,
+});
+
+// save the changes
+await page.save();
+
+

This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar.

+
import { join } from "path";
+import { createReadStream } from "fs";
+import { spfi, SPFI, SPFx } from "@pnp/sp";
+import { SPDefault } from "@pnp/nodejs";
+import { LogLevel  } from "@pnp/logging";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import "@pnp/sp/clientside-pages";
+
+const buffer = readFileSync("c:/temp/key.pem");
+
+const config:any = {
+  auth: {
+    authority: "https://login.microsoftonline.com/{my tenant}/",
+    clientId: "{application (client) id}",
+    clientCertificate: {
+      thumbprint: "{certificate thumbprint, displayed in AAD}",
+      privateKey: buffer.toString(),
+    },
+  },
+  system: {
+    loggerOptions: {
+      loggerCallback(loglevel: any, message: any, containsPii: any) {
+          console.log(message);
+      },
+      piiLoggingEnabled: false,
+      logLevel: LogLevel.Verbose
+    }
+  }
+};
+
+// configure your node options
+const sp = spfi('{site url}').using(SPDefault({
+  baseUrl: '{site url}',
+  msal: {
+    config: config,
+    scopes: [ 'https://{my tenant}.sharepoint.com/.default' ]
+  }
+}));
+
+
+// add the banner image
+const dirname = join("C:/path/to/file", "img-file.jpg");
+
+const chunkedFile = createReadStream(dirname);
+
+const far = await sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents").files.addChunked( "banner.jpg", chunkedFile );
+
+// add the page
+const page = await sp.web.addClientsidePage("MyPage", "Page Title");
+
+// set the banner image
+page.setBannerImage(far.data.ServerRelativeUrl);
+
+// publish the page
+await page.save();
+
+

setBannerImageFromExternalUrl

+

Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there.

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// you must await this method
+await page.setBannerImageFromExternalUrl("https://absolute.url/to/my/image.jpg");
+
+// save the changes
+await page.save();
+
+

You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// you must await this method
+await page.setBannerImageFromExternalUrl("https://absolute.url/to/my/image.jpg", {
+  altText: "Image description",
+  imageSourceType: 2,
+  translateX: 30,
+  translateY: 1234,
+});
+
+// save the changes
+await page.save();
+
+

recycle

+

Allows you to recycle a page without first needing to use getItem

+
// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.recycle();
+
+

delete

+

Allows you to delete a page without first needing to use getItem

+
// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.delete();
+
+

saveAsTemplate

+

Save page as a template from which other pages can be created. If it doesn't exist a special folder "Templates" will be added to the doc lib

+
// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.saveAsTemplate();
+// save a template, but don't publish it allowing you to make changes before it is available to users
+// you 
+await page.saveAsTemplate(false);
+// ... changes to the page
+// you must publish the template so it is available
+await page.save();
+
+

share

+

Allows sharing a page with one or more email addresses, optionall including a message in the email

+
// our page instance
+const page: IClientsidePage;
+// you must await this method
+await page.share(["email@place.com", "email2@otherplace.com"]);
+// optionally include a message
+await page.share(["email@place.com", "email2@otherplace.com"], "Please check out this cool page!");
+
+

Add Repost Page

+

You can use the addRepostPage method to add a report page. The method returns the absolute url of the created page. All properties are optional but it is recommended to include as much as possible to improve the quality of the repost card's display.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages";
+
+const sp = spfi(...);
+const page = await sp.web.addRepostPage({
+    BannerImageUrl: "https://some.absolute/path/to/an/image.jpg",
+    IsBannerImageUrlExternal: true,
+    Description: "My Description",
+    Title: "This is my title!",
+    OriginalSourceUrl: "https://absolute/path/to/article",
+});
+
+
+

To specify an existing item in another list all of the four properties OriginalSourceSiteId, OriginalSourceWebId, OriginalSourceListId, and OriginalSourceItemId are required.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/column-defaults/index.html b/sp/column-defaults/index.html new file mode 100644 index 000000000..b50937d66 --- /dev/null +++ b/sp/column-defaults/index.html @@ -0,0 +1,2966 @@ + + + + + + + + + + + + + + + + + + + + + + + + column defaults - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/column-defaults

+

The column defaults sub-module allows you to manage the default column values on a library or library folder.

+

Selective Imports Banner

+

Get Folder Defaults

+

You can get the default values for a specific folder as shown below:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+const defaults = await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").getDefaultColumnValues();
+
+/*
+The resulting structure will have the form:
+
+[
+  {
+    "name": "{field internal name}",
+    "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
+    "value": "{the default value}"
+  },
+  {
+    "name": "{field internal name}",
+    "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
+    "value": "{the default value}"
+  }
+]
+*/
+
+

Set Folder Defaults

+

When setting the defaults for a folder you need to include the field's internal name and the value.

+
+

For more examples of other field types see the section Pattern for setting defaults on various column types

+

Note: Be very careful when setting the path as the site collection url is case sensitive

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").setDefaultColumnValues([{
+  name: "TextField",
+  value: "Something",
+},
+{
+  name: "NumberField",
+  value: 14,
+}]);
+
+

Get Library Defaults

+

You can also get all of the defaults for the entire library.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+const defaults = await sp.web.lists.getByTitle("DefaultColumnValues").getDefaultColumnValues();
+
+/*
+The resulting structure will have the form:
+
+[
+  {
+    "name": "{field internal name}",
+    "path": "/sites/dev/DefaultColumnValues",
+    "value": "{the default value}"
+  },
+  {
+    "name": "{field internal name}",
+    "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
+    "value": "{a different default value}"
+  }
+]
+*/
+
+

Set Library Defaults

+

You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value.

+
+

For more examples of other field types see the section Pattern for setting defaults on various column types

+

Note: Be very careful when setting the path as the site collection url is case sensitive

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("DefaultColumnValues").setDefaultColumnValues([{
+  name: "TextField",
+  path: "/sites/dev/DefaultColumnValues",
+  value: "#PnPjs Rocks!",
+}]);
+
+

Clear Folder Defaults

+

If you want to clear all of the folder defaults you can use the clear method:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").clearDefaultColumnValues();
+
+

Clear Library Defaults

+

If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/column-defaults";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("DefaultColumnValues").setDefaultColumnValues([]);
+
+

Pattern for setting defaults on various column types

+

The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types.

+
[{
+    // Text/Boolean/CurrencyDateTime/Choice/User
+    name: "TextField":
+    path: "/sites/dev/DefaultColumnValues",
+    value: "#PnPjs Rocks!",
+}, {
+    //Number
+    name: "NumberField",
+    path: "/sites/dev/DefaultColumnValues",
+    value: 42,
+}, {
+    //Date
+    name: "NumberField",
+    path: "/sites/dev/DefaultColumnValues",
+    value: "1900-01-01T00:00:00Z",
+}, {
+    //Date - Today
+    name: "NumberField",
+    path: "/sites/dev/DefaultColumnValues",
+    value: "[today]",
+}, {
+    //MultiChoice
+    name: "MultiChoiceField",
+    path: "/sites/dev/DefaultColumnValues",
+    value: ["Item 1", "Item 2"],
+}, {
+    //MultiChoice - single value
+    name: "MultiChoiceField",
+    path: "/sites/dev/DefaultColumnValues/folder2",
+    value: ["Item 1"],
+}, {
+    //Taxonomy - single value
+    name: "TaxonomyField",
+    path: "/sites/dev/DefaultColumnValues",
+    value: {
+        wssId:"-1",
+        termName: "TaxValueName",
+        termId: "924d2077-d5e3-4507-9f36-4a3655e74274"
+        }
+}, {
+    //Taxonomy - multiple value
+    name: "TaxonomyMultiField",
+    path: "/sites/dev/DefaultColumnValues",
+    value: [{
+        wssId:"-1",
+        termName: "TaxValueName",
+        termId: "924d2077-d5e3-4507-9f36-4a3655e74274"
+        },{
+        wssId:"-1",
+        termName: "TaxValueName2",
+        termId: "95d4c307-dde5-49d8-b861-392e145d94d3"
+        },]
+}]);
+
+

Taxonomy Full Example

+

This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import "@pnp/sp/column-defaults";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get the term's info we want to use as the default
+const term = await sp.termStore.sets.getById("ea6fc521-d293-4f3d-9e84-f3a5bc0936ce").getTermById("775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a")();
+
+// get the default term label
+const defLabel = term.labels.find(v => v.isDefault);
+
+// set the default value using -1, the term id, and the term's default label name
+await sp.web.lists.getByTitle("MetaDataDocLib").rootFolder.setDefaultColumnValues([{
+  name: "MetaDataColumnInternalName",
+  value: {
+      wssId: "-1",
+      termId: term.id,
+      termName: defLabel.name,
+  }
+}])
+
+// check that the defaults have updated
+const newDefaults = await sp.web.lists.getByTitle("MetaDataDocLib").getDefaultColumnValues();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/comments-likes/index.html b/sp/comments-likes/index.html new file mode 100644 index 000000000..36d3ed876 --- /dev/null +++ b/sp/comments-likes/index.html @@ -0,0 +1,3151 @@ + + + + + + + + + + + + + + + + + + + + + + + + comments and likes - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/comments and likes

+

Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles.

+

These APIs are currently in BETA and are subject to change or may not work on all tenants.

+

Invokable Banner Selective Imports Banner

+

ClientsidePage Comments

+

The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.

+

Add Comments

+

You can add a comment using the addComment method as shown

+
import { spfi } from "@pnp/sp";
+import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
+// optionally publish the page first
+await page.save();
+
+//add a comment as text
+const comment = await page.addComment("A test comment");
+
+//or you can include the @mentions. html anchor required to include mention in text body.
+const mentionHtml = `<a data-sp-mention-user-id="test@contoso.com" href="mailto&#58;test@contoso.com.com" tabindex="-1">Test User</a>`;
+
+const commentInfo: Partial<ICommentInfo> = { text: `${mentionHtml} This is the test comment with at mentions`, 
+    mentions: [{ loginName: 'test@contoso.com', email: 'test@contoso.com', name: 'Test User' }], };
+const comment = await page.addComment(commentInfo);
+
+

Get Page Comments

+
import { spfi } from "@pnp/sp";
+import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
+// optionally publish the page first
+await page.save();
+
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+await page.addComment("A test comment");
+
+const comments = await page.getComments();
+
+

enableComments & disableComments

+

Used to control the availability of comments on a page

+
import { spfi } from "@pnp/sp";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+// you need to import the comments sub-module or use the all preset
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// our page instance
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// turn on comments
+await page.enableComments();
+
+// turn off comments
+await page.disableComments();
+
+

GetById

+
import { spfi } from "@pnp/sp";
+import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
+// optionally publish the page first
+await page.save();
+
+const comment = await page.addComment("A test comment");
+
+const commentData = await page.getCommentById(parseInt(comment.id, 10));
+
+

Clear Comments

+

Item Comments

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+import "@pnp/sp/items";
+import "@pnp/sp/comments/item";
+
+const sp = spfi(...);
+
+const item = await sp.web.getFileByServerRelativePath("/sites/dev/SitePages/Test_8q5L.aspx").getItem();
+
+// as an example, or any of the below options
+await item.like();
+
+

The below examples use a variable named "item" which is taken to represent an IItem instance.

+

Comments

+

Get Item Comments

+
const comments = await item.comments();
+
+

You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:

+
import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+// these will be Comment instances in the array
+comments[0].replies.add({ text: "#PnPjs is pretty ok!" });
+
+//load the top 20 replies and comments for an item including likedBy information
+const comments = await item.comments.expand("replies", "likedBy", "replies/likedBy").top(20)();
+
+

Add Comment

+
import { spfi } from "@pnp/sp";
+import { ICommentInfo } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+// you can add a comment as a string
+const comment = await item.comments.add("string comment");
+
+
+
+

Delete a Comment

+
import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+// these will be Comment instances in the array
+comments[0].delete()
+
+

Like Comment

+
import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+// these will be Comment instances in the array
+comments[0].like();
+
+

Unlike Comment

+
import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+comments[0].unlike()
+
+

Reply to a Comment

+
import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+const comment = await comments[0].comments.add({ text: "#PnPjs is pretty ok!" });
+
+

Load Replies to a Comment

+
import { spfi } from "@pnp/sp";
+import { IComments } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const comments: IComments = await item.comments();
+
+const replies = await comments[0].replies();
+
+

Like/Unlike

+

You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/comments";
+import { ILikeData, ILikedByInformation } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const item = sp.web.lists.getByTitle("PnP List").items.getById(1);
+
+// like an item
+await item.like();
+
+// unlike an item
+await item.unlike();
+
+// get the liked by information
+const likedByInfo: ILikedByInformation = await item.getLikedByInformation();
+
+

To like/unlike a client-side page and get liked by information.

+
import { spfi } from "@pnp/sp";
+import { ILikedByInformation } from "@pnp/sp/comments";
+import { IClientsidePage } from "@pnp/sp/clientside-pages";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages";
+import "@pnp/sp/comments/clientside-page";
+
+const sp = spfi(...);
+
+const page: IClientsidePage = await sp.web.loadClientsidePage("/sites/dev/sitepages/home.aspx");
+
+// like a page
+await page.like();
+
+// unlike a page
+await page.unlike();
+
+// get the liked by information
+const likedByInfo: ILikedByInformation = await page.getLikedByInformation();
+
+

Rate

+

You can rate list items with a numeric values between 1 and 5.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/comments";
+import { ILikeData, ILikedByInformation } from "@pnp/sp/comments";
+
+const sp = spfi(...);
+
+const item = sp.web.lists.getByTitle("PnP List").items.getById(1);
+
+// rate an item
+await item.rate(2);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/content-types/index.html b/sp/content-types/index.html new file mode 100644 index 000000000..ab5c85d23 --- /dev/null +++ b/sp/content-types/index.html @@ -0,0 +1,2906 @@ + + + + + + + + + + + + + + + + + + + + + + + + content types - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/content-types

+

Content Types are used to define sets of columns in SharePoint.

+

IContentTypes

+

Invokable Banner Selective Imports Banner

+

Add an existing Content Type to a collection

+

The following example shows how to add the built in Picture Content Type to the Documents library.

+
const sp = spfi(...);
+
+sp.web.lists.getByTitle("Documents").contentTypes.addAvailableContentType("0x010102");
+
+

Get a Content Type by Id

+
import { IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+const d: IContentType = await sp.web.contentTypes.getById("0x01")();
+
+// log content type name to console
+console.log(d.name);
+
+

Update a Content Type

+
import { IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+await sp.web.contentTypes.getById("0x01").update({EditFormClientSideComponentId: "9dfdb916-7380-4b69-8d92-bc711f5fa339"});
+
+

Add a new Content Type

+

To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.

+
const sp = spfi(...);
+
+sp.web.contentTypes.add("0x01008D19F38845B0884EBEBE239FDF359184", "My Content Type");
+
+

It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API).

+
const sp = spfi(...);
+
+//Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings)
+sp.web.contentTypes.add("0x01008D19F38845B0884EBEBE239FDF359184", "My Content Type", "This is my content type.", "_PnP Content Types", { ReadOnly: true });
+
+

IContentType

+

Invokable Banner Selective Imports Banner

+ +

Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get field links from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").fieldLinks();
+
+// log collection of fieldlinks to console
+console.log(d);
+
+

Get Content Type fields

+

To get a collection with all fields on the Content Type, simply use this method.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get fields from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").fields();
+
+// log collection of fields to console
+console.log(d);
+
+

Get parent Content Type

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get parent Content Type from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").parent();
+
+// log name of parent Content Type to console
+console.log(d.Name)
+
+

Get Content Type Workflow associations

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { ContentType, IContentType } from "@pnp/sp/content-types";
+
+const sp = spfi(...);
+
+// get workflow associations from built in Content Type Document (Id: "0x0101")
+const d = await sp.web.contentTypes.getById("0x0101").workflowAssociations();
+
+// log collection of workflow associations to console
+console.log(d);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/context-info/index.html b/sp/context-info/index.html new file mode 100644 index 000000000..d2d9ed29e --- /dev/null +++ b/sp/context-info/index.html @@ -0,0 +1,2753 @@ + + + + + + + + + + + + + + + + + + + + + + + + context info - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/ - context-info

+

Selective Imports Banner

+

Starting with 3.8.0 we've moved context information to its own sub-module. You can now import context-info and use it on any SPQueryable derived object to understand the context. Some examples are below.

+

IContextInfo

+

The information returned by the method is defined by the IContextInfo interface.

+
export interface IContextInfo {
+    FormDigestTimeoutSeconds: number;
+    FormDigestValue: number;
+    LibraryVersion: string;
+    SiteFullUrl: string;
+    SupportedSchemaVersions: string[];
+    WebFullUrl: string;
+}
+
+

Get Context for a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/context-info";
+
+const sp = spfi(...);
+
+const info = await sp.web.getContextInfo();
+
+

Get Context from lists

+

This pattern works as well for any SPQueryable derived object, allowing you to gain context no matter with which fluent objects you are working.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/context-info";
+
+const sp = spfi(...);
+
+const info = await sp.web.lists.getContextInfo();
+
+

Get Context from URL

+

Often you will have an absolute URL to a file or path and would like to create an IWeb or IFile. You can use the fileFromPath or folderFromPath to get an IFile/IFolder, or you can use getContextInfo to create a new web within the context of the file path.

+
import { spfi } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+import "@pnp/sp/context-info";
+
+const sp = spfi(...);
+
+// supply an absolute path to get associated context info, this works across site collections
+const { WebFullUrl } = await sp.web.getContextInfo("https://tenant.sharepoint.com/sites/dev/shared documents/file.docx");
+
+// create a new web pointing to the web where the file is stored
+const web = Web([sp.web, decodeURI(WebFullUrl)]);
+
+const webInfo = await web();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/favorites/index.html b/sp/favorites/index.html new file mode 100644 index 000000000..cf35dc56e --- /dev/null +++ b/sp/favorites/index.html @@ -0,0 +1,2802 @@ + + + + + + + + + + + + + + + + + + + + + + + + Favorites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/ - favorites

+

Selective Imports Banner

+

The favorites API allows you to fetch and manipulate followed sites and list items (also called saved for later). Note, all of these methods only work with the context of a logged in user, and not with app-only permissions.

+

Get current user's followed sites

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const favSites = await sp.favorites.getFollowedSites();
+
+

Add a site to current user's followed sites

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const tenantUrl = "contoso.sharepoint.com";
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1"
+const webUrl = "https://contoso.sharepoint.com/sites/favsite"
+
+const favSiteInfo = await sp.favorites.getFollowedSites.add(tenantUrl, siteId, webId, webUrl);
+
+

Remove a site from current user's followed sites

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const tenantUrl = "contoso.sharepoint.com";
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1"
+const webUrl = "https://contoso.sharepoint.com/sites/favsite"
+
+await sp.favorites.getFollowedSites.remove(tenantUrl, siteId, webId, webUrl);
+
+

Get current user's followed list items

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const favListItems = await sp.favorites.getFollowedListItems();
+
+

Add an item to current user's followed list items

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1";
+const listId = "f09fe67e-0160-4fcc-9144-905bd4889f31";
+const listItemUniqueId = "1425C841-626A-44C9-8731-DA8BDC0882D1";
+
+const favListItemInfo = await sp.favorites.getFollowedListItems.add(siteId, webId, listId, listItemUniqueId);
+
+

Remove an item from current user's followed list items

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/favorites";
+
+const sp = spfi(...);
+
+const siteId = "e3913de9-bfee-4089-b1bc-fb147d302f11";
+const webId = "11a53c2b-0a67-46c8-8599-db50b8bc4dd1";
+const listId = "f09fe67e-0160-4fcc-9144-905bd4889f31";
+const listItemUniqueId = "1425C841-626A-44C9-8731-DA8BDC0882D1";
+
+const favListItemInfo = await sp.favorites.getFollowedListItems.remove(siteId, webId, listId, listItemUniqueId);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/features/index.html b/sp/features/index.html new file mode 100644 index 000000000..432f4ac93 --- /dev/null +++ b/sp/features/index.html @@ -0,0 +1,2815 @@ + + + + + + + + + + + + + + + + + + + + + + + + Features - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/features

+

Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.

+

IFeatures

+

Invokable Banner Selective Imports Banner

+

Represents a collection of features. SharePoint Sites and Webs will have a collection of features

+

getById

+

Gets the information about a feature for the given GUID

+
import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+const webFeature = await sp.web.features.getById(webFeatureId)();
+
+const siteFeatureId = "guid-of-site-scope-feature";
+const siteFeature = await sp.site.features.getById(siteFeatureId)();
+
+

add

+

Adds (activates) a feature at the Site or Web level

+
import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+let res = await sp.web.features.add(webFeatureId);
+// Activate with force
+res = await sp.web.features.add(webFeatureId, true);
+
+

remove

+

Removes and deactivates the specified feature from the SharePoint Site or Web

+
import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+let res = await sp.web.features.remove(webFeatureId);
+// Deactivate with force
+res = await sp.web.features.remove(webFeatureId, true);
+
+

IFeature

+

Represents an instance of a SharePoint feature.

+

Invokable Banner Selective Imports Banner

+

deactivate

+

Deactivates the specified feature from the SharePoint Site or Web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/features";
+
+const sp = spfi(...);
+
+//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
+const webFeatureId = "guid-of-web-feature";
+sp.web.features.remove(webFeatureId);
+
+// Deactivate with force
+sp.web.features.remove(webFeatureId, true);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/fields/index.html b/sp/fields/index.html new file mode 100644 index 000000000..541986d9a --- /dev/null +++ b/sp/fields/index.html @@ -0,0 +1,3610 @@ + + + + + + + + + + + + + + + + + + + + + + + + Fields - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/fields

+

Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.

+

IFields

+

Invokable Banner Selective Imports Banner

+

Get Field by Id

+

Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

+
import { spfi } from "@pnp/sp";
+import { IField, IFieldInfo } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/fields";
+
+// set up sp root object
+const sp = spfi(...);
+// get the field by Id for web
+const field: IField = sp.web.fields.getById("03b05ff4-d95d-45ed-841d-3855f77a2483");
+// get the field by Id for list 'My List'
+const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getById("03b05ff4-d95d-45ed-841d-3855f77a2483")();
+
+// we can use this 'field' variable to execute more queries on the field:
+const r = await field.select("Title")();
+
+// show the response from the server
+console.log(r.Title);
+
+

Get Field by Title

+

You can also get a field from the collection by title.

+
import { spfi } from "@pnp/sp";
+import { IField, IFieldInfo } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists"
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+// get the field with the title 'Author' for web
+const field: IField = sp.web.fields.getByTitle("Author");
+// get the field with the title 'Title' for list 'My List'
+const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getByTitle("Title")();
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Get Field by Internal Name or Title

+

You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different.

+
import { spfi } from "@pnp/sp";
+import { IField, IFieldInfo } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists"
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+// get the field with the internal name 'ModifiedBy' for web
+const field: IField = sp.web.fields.getByInternalNameOrTitle("ModifiedBy");
+// get the field with the internal name 'ModifiedBy' for list 'My List'
+const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getByInternalNameOrTitle("ModifiedBy")();
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Create a Field using an XML schema

+

Create a new field by defining an XML schema that assigns all the properties for the field.

+
import { spfi } from "@pnp/sp";
+import { IField, IFieldAddResult } from "@pnp/sp/fields/types";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// define the schema for your new field, in this case a date field with a default date of today.
+const fieldSchema = `<Field ID="{03b09ff4-d99d-45ed-841d-3855f77a2483}" StaticName="MyField" Name="MyField" DisplayName="My New Field" FriendlyDisplayFormat="Disabled" Format="DateOnly" Type="DateTime" Group="My Group"><Default>[today]</Default></Field>`;
+
+// create the new field in the web
+const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema);
+// create the new field in the list 'My List'
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(fieldSchema);
+
+// we can use this 'field' variable to run more queries on the list:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a New Field

+

Use the add method to create a new field where you define the field type

+
import { spfi } from "@pnp/sp";
+import { IField, IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.add("My Field", FieldTypes.Text, { FieldTypeKind: 3, Group: "My Group" });
+// create a new field called 'My Field' in the list 'My List'
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.add("My Field", FieldTypes.Text, { FieldTypeKind: 3, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Site Field to a List

+

Use the createFieldAsXml method to add a site field to a list.

+
import { spfi } from "@pnp/sp";
+import { IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.add("My Field", FieldTypes.Text, { FieldTypeKind: 3, Group: "My Group" });
+// add the site field 'My Field' to the list 'My List'
+const r = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(field.data.SchemaXml as string);
+
+// log the field Id to console
+console.log(r.data.Id);
+
+

Add a Text Field

+

Use the addText method to create a new text field.

+
import { spfi } from "@pnp/sp";
+import { IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new text field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.addText("My Field", { MaxLength: 255, Group: "My Group" });
+// create a new text field called 'My Field' in the list 'My List'.
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.addText("My Field", { MaxLength: 255, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Calculated Field

+

Use the addCalculated method to create a new calculated field.

+
import { spfi } from "@pnp/sp";
+import { DateTimeFieldFormatType, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new calculated field called 'My Field' in web
+const field = await sp.web.fields.addCalculated("My Field", { Formula: "=Modified+1", DateFormat: DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: "MyGroup" });
+// create a new calculated field called 'My Field' in the list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addCalculated("My Field", { Formula: "=Modified+1", DateFormat:  DateTimeFieldFormatType.DateOnly, FieldTypeKind: FieldTypes.Calculated, Group: "MyGroup" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Date/Time Field

+

Use the addDateTime method to create a new date/time field.

+
import { spfi } from "@pnp/sp";
+import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new date/time field called 'My Field' in web
+const field = await sp.web.fields.addDateTime("My Field", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled,  Group: "My Group" });
+// create a new date/time field called 'My Field' in the list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addDateTime("My Field", { DisplayFormat: DateTimeFieldFormatType.DateOnly, DateTimeCalendarType: CalendarType.Gregorian, FriendlyDisplayFormat: DateTimeFieldFriendlyFormatType.Disabled, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Currency Field

+

Use the addCurrency method to create a new currency field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new currency field called 'My Field' in web
+const field = await sp.web.fields.addCurrency("My Field", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: "My Group" });
+// create a new currency field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addCurrency("My Field", { MinimumValue: 0, MaximumValue: 100, CurrencyLocaleId: 1033, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add an Image Field

+

Use the addImageField method to create a new image field.

+
import { spfi } from "@pnp/sp";
+import { IFieldAddResult, FieldTypes } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new image field called 'My Field' in web.
+const field: IFieldAddResult = await sp.web.fields.addImageField("My Field");
+// create a new image field called 'My Field' in the list 'My List'.
+const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.addImageField("My Field");
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Multi-line Text Field

+

Use the addMultilineText method to create a new multi-line text field.

+
+

For Enhanced Rich Text mode, see the next section.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new multi-line text field called 'My Field' in web
+const field = await sp.web.fields.addMultilineText("My Field", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: "My Group" });
+// create a new multi-line text field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addMultilineText("My Field", { NumberOfLines: 6, RichText: true, RestrictedMode: false, AppendOnly: false, AllowHyperlink: true, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Multi-line Text Field with Enhanced Rich Text

+

The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+//Create a new multi-line text field called 'My Field' in web
+const field = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(
+    `<Field Type="Note" Name="MyField" DisplayName="My Field" Required="FALSE" RichText="TRUE" RichTextMode="FullHtml" />`
+);
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Number Field

+

Use the addNumber method to create a new number field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new number field called 'My Field' in web
+const field = await sp.web.fields.addNumber("My Field", { MinimumValue: 1, MaximumValue: 100, Group: "My Group" });
+// create a new number field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addNumber("My Field", { MinimumValue: 1, MaximumValue: 100, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a URL Field

+

Use the addUrl method to create a new url field.

+
import { spfi } from "@pnp/sp";
+import { UrlFieldFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new url field called 'My Field' in web
+const field = await sp.web.fields.addUrl("My Field", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: "My Group" });
+// create a new url field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addUrl("My Field", { DisplayFormat: UrlFieldFormatType.Hyperlink, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a User Field

+

Use the addUser method to create a new user field.

+
import { spfi } from "@pnp/sp";
+import { FieldUserSelectionMode } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new user field called 'My Field' in web
+const field = await sp.web.fields.addUser("My Field", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: "My Group" });
+// create a new user field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addUser("My Field", { SelectionMode: FieldUserSelectionMode.PeopleOnly, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+// **
+// Adding a lookup that supports multiple values takes two calls:
+const fieldAddResult = await sp.web.fields.addUser("Multi User Field", { SelectionMode: FieldUserSelectionMode.PeopleOnly });
+await fieldAddResult.field.update({ AllowMultipleValues: true }, "SP.FieldUser");
+
+

Add a Lookup Field

+

Use the addLookup method to create a new lookup field.

+
import { spfi } from "@pnp/sp";
+import { FieldTypes } from "@pnp/sp/fields/types";
+
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const list = await sp.web.lists.getByTitle("My Lookup List")();
+// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web.
+const field = await sp.web.fields.addLookup("My Field", { LookupListId: list.data.Id, LookupFieldName: "Title" });
+// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addLookup("My Field", {LookupListId: list.data.Id, LookupFieldName: "Title"});
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+// **
+// Adding a lookup that supports multiple values takes two calls:
+const fieldAddResult = await sp.web.fields.addLookup("Multi Lookup Field", { LookupListId: list.data.Id, LookupFieldName: "Title" });
+await fieldAddResult.field.update({ AllowMultipleValues: true }, "SP.FieldLookup");
+
+

Add a Choice Field

+

Use the addChoice method to create a new choice field.

+
import { spfi } from "@pnp/sp";
+import { ChoiceFieldFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];
+// create a new choice field called 'My Field' in web
+const field = await sp.web.fields.addChoice("My Field", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: "My Group" });
+// create a new choice field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addChoice("My Field", { Choices: choices, EditFormat: ChoiceFieldFormatType.Dropdown, FillInChoice: false, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Multi-Choice Field

+

Use the addMultiChoice method to create a new multi-choice field.

+
import { spfi } from "@pnp/sp";
+import { ChoiceFieldFormatType } from "@pnp/sp/fields/types";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];
+// create a new multi-choice field called 'My Field' in web
+const field = await sp.web.fields.addMultiChoice("My Field", { Choices: choices, FillInChoice: false, Group: "My Group" });
+// create a new multi-choice field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addMultiChoice("My Field", { Choices: choices, FillInChoice: false, Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Boolean Field

+

Use the addBoolean method to create a new boolean field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new boolean field called 'My Field' in web
+const field = await sp.web.fields.addBoolean("My Field", { Group: "My Group" });
+// create a new boolean field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addBoolean("My Field", { Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Dependent Lookup Field

+

Use the addDependentLookupField method to create a new dependent lookup field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const field = await sp.web.fields.addLookup("My Field", { LookupListId: list.Id, LookupFieldName: "Title" });
+// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web.
+const fieldDep = await sp.web.fields.addDependentLookupField("My Dep Field", field.data.Id as string, "Description");
+// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addLookup("My Field", { LookupListId: list.Id, LookupFieldName: "Title" });
+const fieldDep2 = await sp.web.lists.getByTitle("My List").fields.addDependentLookupField("My Dep Field", field2.data.Id as string, "Description");
+
+// we can use this 'fieldDep' variable to run more queries on the field:
+const r = await fieldDep.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Add a Location Field

+

Use the addLocation method to create a new location field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// create a new location field called 'My Field' in web
+const field = await sp.web.fields.addLocation("My Field", { Group: "My Group" });
+// create a new location field called 'My Field' in list 'My List'
+const field2 = await sp.web.lists.getByTitle("My List").fields.addLocation("My Field", { Group: "My Group" });
+
+// we can use this 'field' variable to run more queries on the field:
+const r = await field.field.select("Id")();
+
+// log the field Id to console
+console.log(r.Id);
+
+

Delete a Field

+

Use the delete method to delete a field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+await sp.web.fields.addBoolean("Temp Field", { Group: "My Group" });
+await sp.web.fields.addBoolean("Temp Field 2", { Group: "My Group" });
+await sp.web.lists.getByTitle("My List").fields.addBoolean("Temp Field", { Group: "My Group" });
+await sp.web.lists.getByTitle("My List").fields.addBoolean("Temp Field 2", { Group: "My Group" });
+
+// delete one or more fields from web, returns boolean
+const result = await sp.web.fields.getByTitle("Temp Field").delete();
+const result2 = await sp.web.fields.getByTitle("Temp Field 2").delete();
+
+
+// delete one or more fields from list 'My List', returns boolean
+const result = await sp.web.lists.getByTitle("My List").fields.getByTitle("Temp Field").delete();
+const result2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("Temp Field 2").delete();
+
+

Update a Field

+

Use the update method to update a field.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// update the field called 'My Field' with a description in web, returns FieldUpdateResult
+const fieldUpdate = await sp.web.fields.getByTitle("My Field").update({ Description: "My Description" });
+// update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult
+const fieldUpdate2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").update({ Description: "My Description" });
+
+// if you need to update a field with properties for a specific field type you can optionally include the field type as a second param
+// if you do not include it we will look up the type, but that adds a call to the server
+const fieldUpdate2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Look up Field").update({ RelationshipDeleteBehavior: 1 }, "SP.FieldLookup");
+
+

Show a Field in the Display Form

+

Use the setShowInDisplayForm method to add a field to the display form.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// show field called 'My Field' in display form throughout web
+await sp.web.fields.getByTitle("My Field").setShowInDisplayForm(true);
+// show field called 'My Field' in display form for list 'My List'
+await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInDisplayForm(true);
+
+

Show a Field in the Edit Form

+

Use the setShowInEditForm method to add a field to the edit form.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// show field called 'My Field' in edit form throughout web
+await sp.web.fields.getByTitle("My Field").setShowInEditForm(true);
+// show field called 'My Field' in edit form for list 'My List'
+await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInEditForm(true);
+
+

Show a Field in the New Form

+

Use the setShowInNewForm method to add a field to the display form.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// show field called 'My Field' in new form throughout web
+await sp.web.fields.getByTitle("My Field").setShowInNewForm(true);
+// show field called 'My Field' in new form for list 'My List'
+await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInNewForm(true);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/files/index.html b/sp/files/index.html new file mode 100644 index 000000000..d9d78a62b --- /dev/null +++ b/sp/files/index.html @@ -0,0 +1,3512 @@ + + + + + + + + + + + + + + + + + + + + + + + + Files - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/files

+

One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.

+

Reading Files

+

Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const blob: Blob = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.avi").getBlob();
+
+const buffer: ArrayBuffer = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.avi").getBuffer();
+
+const json: any = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.json").getJSON();
+
+const text: string = await sp.web.getFileByServerRelativePath("/sites/dev/documents/file.txt").getText();
+
+// all of these also work from a file object no matter how you access it
+const text2: string = await sp.web.getFolderByServerRelativePath("/sites/dev/documents").files.getByUrl("file.txt").getText();
+
+

getFileByUrl

+

This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+
+const sp = spfi(...);
+
+const url = "{absolute file url OR sharing url}";
+
+// file is an IFile and supports all the file operations
+const file = sp.web.getFileByUrl(url);
+
+// for example
+const fileContent = await file.getText();
+
+

fileFromServerRelativePath

+

Added in 3.3.0

+

Utility method allowing you to get an IFile reference using any SPQueryable as a base and the server relative path to the file. Helpful when you do not have convenient access to an IWeb to use getFileByServerRelativePath.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { fileFromServerRelativePath } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const url = "/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file = fileFromServerRelativePath(sp.web, url);
+
+// for example
+const fileContent = await file.getText();
+
+

fileFromAbsolutePath

+

Added in 3.8.0

+

Batching Not Supported Banner

+

Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.

+
+

Works across site collections within the same tenant

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { fileFromAbsolutePath } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file = fileFromAbsolutePath(sp.web, url);
+
+// for example
+const fileContent = await file.getText();
+
+

fileFromPath

+

Added in 3.8.0

+

Batching Not Supported Banner

+

Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute OR server relative path to the file.

+
+

Works across site collections within the same tenant

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { fileFromPath } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file = fileFromPath(sp.web, url);
+
+// for example
+const fileContent = await file.getText();
+
+const url2 = "/sites/dev/documents/file.txt";
+
+// file is an IFile and supports all the file operations
+const file2 = fileFromPath(sp.web, url2);
+
+// for example
+const fileContent2 = await file2.getText();
+
+

Adding Files

+

Likewise you can add files using one of two methods, addUsingPath or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.

+

The addUsingPath method, supports the percent or pound characters in file names.

+

When using EnsureUniqueFileName property, you must omit the Overwrite parameter.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+//Sample uses pure JavaScript to access the input tag of type="file" ->https://www.w3schools.com/tags/att_input_type_file.asp 
+let file = <HTMLInputElement>document.getElementById("thefileinput");
+const fileNamePath = encodeURI(file.name);
+let result: IFileAddResult;
+// you can adjust this number to control what size files are uploaded in chunks
+if (file.size <= 10485760) {
+    // small upload
+    result = await sp.web.getFolderByServerRelativePath("Shared Documents").files.addUsingPath(fileNamePath, file, { Overwrite: true });
+} else {
+    // large upload
+    result = await sp.web.getFolderByServerRelativePath("Shared Documents").files.addChunked(fileNamePath, file, data => {
+    console.log(`progress`);
+    }, true);
+}
+
+console.log(`Result of file upload: ${JSON.stringify(result)}`);
+
+

Adding a file using Nodejs Streams

+

If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams.

+

Batching Not Supported Banner

+
// triggers auto-application of extensions, in this case to add getStream
+import { spfi } from "@pnp/sp";
+import "@pnp/nodejs";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/folders/list";
+import "@pnp/sp/files/folder";
+import { createReadStream } from 'fs';
+
+// get a stream of an existing file
+const stream = createReadStream("c:/temp/file.txt");
+
+// now add the stream as a new file
+const sp = spfi(...);
+
+const fr = await sp.web.lists.getByTitle("Documents").rootFolder.files.addChunked( "new.txt", stream, undefined, true );
+
+

Setting Associated Item Values

+

You can also update the file properties of a newly uploaded file using code similar to the below snippet:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+const file = await sp.web.getFolderByServerRelativePath("/sites/dev/Shared%20Documents/test/").files.addUsingPath("file.name", "content", {Overwrite: true});
+const item = await file.file.getItem();
+await item.update({
+  Title: "A Title",
+  OtherField: "My Other Value"
+});
+
+

Update File Content

+

You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file.

+

Batching Not Supported Banner

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+await sp.web.getFileByServerRelativePath("/sites/dev/documents/test.txt").setContent("New string content for the file.");
+
+await sp.web.getFileByServerRelativePath("/sites/dev/documents/test.mp4").setContentChunked(file);
+
+

Check in, Check out, and Approve & Deny

+

The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.

+

Check In

+

Check in takes two optional arguments, comment and check in type.

+
import { spfi } from "@pnp/sp";
+import { CheckinType } from "@pnp/sp/files";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// default options with empty comment and CheckinType.Major
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkin();
+console.log("File checked in!");
+
+// supply a comment (< 1024 chars) and using default check in type CheckinType.Major
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkin("A comment");
+console.log("File checked in!");
+
+// Supply both comment and check in type
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkin("A comment", CheckinType.Overwrite);
+console.log("File checked in!");
+
+

Check Out

+

Check out takes no arguments.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").checkout();
+console.log("File checked out!");
+
+

Approve and Deny

+

You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").approve("Approval Comment");
+console.log("File approved!");
+
+// deny with no comment
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").deny();
+console.log("File denied!");
+
+// deny with a supplied comment.
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").deny("Deny comment");
+console.log("File denied!");
+
+

Publish and Unpublish

+

You can both publish and unpublish a file using the library. Both methods take an optional comment argument.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// publish with no comment
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").publish();
+console.log("File published!");
+
+// publish with a supplied comment.
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").publish("Publish comment");
+console.log("File published!");
+
+// unpublish with no comment
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").unpublish();
+console.log("File unpublished!");
+
+// unpublish with a supplied comment.
+await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/file.txt").unpublish("Unpublish comment");
+console.log("File unpublished!");
+
+

Advanced Upload Options

+

Both the addChunked and setContentChunked methods support options beyond just supplying the file content.

+

Batching Not Supported Banner

+

progress function

+

A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:

+

(data: ChunkedFileUploadProgressData) => void

+

The data interface is:

+
export interface ChunkedFileUploadProgressData {
+    stage: "starting" | "continue" | "finishing";
+    blockNumber: number;
+    totalBlocks: number;
+    chunkSize: number;
+    currentPointer: number;
+    fileSize: number;
+}
+
+

chunkSize

+

This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.

+

getItem

+

This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.

+
import { spFI, SPFx } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const item = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem();
+console.log(item);
+
+const item2 = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem("Title", "Modified");
+console.log(item2);
+
+// you can also chain directly off this item instance
+const perms = await item.getCurrentUserEffectivePermissions();
+console.log(perms);
+
+

You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.

+
import { spFI, SPFx } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import "@pnp/sp/folders";
+import "@pnp/sp/items";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+// also supports typing the objects so your type will be a union type
+const item = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem<{ Id: number, Title: string }>("Id", "Title");
+
+// You get intellisense and proper typing of the returned object
+console.log(`Id: ${item.Id} -- ${item.Title}`);
+
+// You can also chain directly off this item instance
+const perms = await item.getCurrentUserEffectivePermissions();
+console.log(perms);
+
+

move by path

+

It's possible to move a file to a new destination within a site collection

+
+

If you change the filename during the move operation this is considered an "edit" and the file's modified information will be updated regardless of the "RetainEditorAndModifiedOnMove" setting.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").moveByPath(destinationUrl, false, true);
+
+

Added in 3.7.0

+

You can also supply a set of detailed options to better control the move process:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/new-file.docx").moveByPath(destinationUrl, false, {
+    KeepBoth: false,
+    RetainEditorAndModifiedOnMove: true,
+    ShouldBypassSharedLocks: false,
+});
+
+

copy

+

It's possible to copy a file to a new destination within a site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyTo(destinationUrl, false);
+
+

copy by path

+

It's possible to copy a file to a new destination within the same or a different site collection.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyByPath(destinationUrl, false, true);
+
+

Added in 3.7.0

+

You can also supply a set of detailed options to better control the copy process:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
+
+await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyByPath(destinationUrl, false, {
+    KeepBoth: false,
+    ResetAuthorAndCreatedOnCopy: true,
+    ShouldBypassSharedLocks: false,
+});
+
+

getFileById

+

You can get a file by Id from a web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+import { IFile } from "@pnp/sp/files";
+
+const sp = spfi(...);
+
+const file: IFile = sp.web.getFileById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
+
+

delete

+

Deletes a file

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("filename.txt").delete();
+
+

delete with params

+

Deletes a file with options

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("filename.txt").deleteWithParams({
+    BypassSharedLock: true,
+});
+
+

exists

+

Checks to see if a file exists

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+const exists = await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("name.txt").exists();
+
+

lockedByUser

+

Gets the user who currently has this file locked for shared use

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+const user = await sp.web.getFolderByServerRelativePath("{folder relative path}").files.getByUrl("name.txt").getLockedByUser();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/folders/index.html b/sp/folders/index.html new file mode 100644 index 000000000..c17011631 --- /dev/null +++ b/sp/folders/index.html @@ -0,0 +1,3497 @@ + + + + + + + + + + + + + + + + + + + + + + + + Folders - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/folders

+

Folders serve as a container for your files and list items.

+

IFolders

+

Invokable Banner Selective Imports Banner

+

Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties.

+

Get folders collection for various SharePoint objects

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/items";
+import "@pnp/sp/folders";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// gets web's folders
+const webFolders = await sp.web.folders();
+
+// gets list's folders
+const listFolders = await sp.web.lists.getByTitle("My List").rootFolder.folders();
+
+// gets item's folders
+const itemFolders = await sp.web.lists.getByTitle("My List").items.getById(1).folder.folders();
+
+

folderFromServerRelativePath

+

Added in 3.3.0

+

Utility method allowing you to get an IFolder reference using any SPQueryable as a base and the server relative path to the folder. Helpful when you do not have convenient access to an IWeb to use getFolderByServerRelativePath.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { folderFromServerRelativePath } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const url = "/sites/dev/documents/folder4";
+
+// file is an IFile and supports all the file operations
+const folder = folderFromServerRelativePath(sp.web, url);
+
+

folderFromAbsolutePath

+

Added in 3.8.0

+

Utility method allowing you to get an IFile reference using any SPQueryable as a base and an absolute path to the file.

+
+

Works across site collections within the same tenant

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { folderFromAbsolutePath } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/folder";
+
+// file is an IFile and supports all the file operations
+const folder = folderFromAbsolutePath(sp.web, url);
+
+// for example
+const folderInfo = await folder();
+
+

folderFromPath

+

Added in 3.8.0

+

Utility method allowing you to get an IFolder reference using any SPQueryable as a base and an absolute OR server relative path to the file.

+
+

Works across site collections within the same tenant

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { folderFromPath } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const url = "https://tenant.sharepoint.com/sites/dev/documents/folder";
+
+// file is an IFile and supports all the file operations
+const folder = folderFromPath(sp.web, url);
+
+// for example
+const folderInfo = await folder();
+
+const url2 = "/sites/dev/documents/folder";
+
+// file is an IFile and supports all the file operations
+const folder2 = folderFromPath(sp.web, url2);
+
+// for example
+const folderInfo2 = await folder2();
+
+

add

+

Adds a new folder to collection of folders

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// creates a new folder for web with specified url
+const folderAddResult = await sp.web.folders.addUsingPath("folder url");
+
+

getByUrl

+

Gets a folder instance from a collection by folder's name

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder = await sp.web.folders.getByUrl("folder name")();
+
+

IFolder

+

Represents an instance of a SharePoint folder.

+

Invokable Banner Selective Imports Banner

+

Get a folder object associated with different SharePoint artifacts (web, list, list item)

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// web's folder
+const rootFolder = await sp.web.rootFolder();
+
+// list's folder
+const listRootFolder = await sp.web.lists.getByTitle("234").rootFolder();
+
+// item's folder
+const itemFolder = await sp.web.lists.getByTitle("234").items.getById(1).folder();
+
+

getItem

+

Gets list item associated with a folder

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folderItem = await sp.web.rootFolder.folders.getByUrl("SiteAssets").folders.getByUrl("My Folder").getItem();
+
+

storageMetrics

+

Added in 3.8.0

+

Gets a set of metrics describing the total file size contained in the folder.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const metrics = await sp.web.getFolderByServerRelativePath("/sites/dev/shared documents/target").storageMetrics();
+
+// you can also select specific metrics if desired:
+const metrics2 = await sp.web.getFolderByServerRelativePath("/sites/dev/shared documents/target").storageMetrics.select("TotalSize")();
+
+

move by path

+

It's possible to move a folder to a new destination within the same or a different site collection

+
+

If you change the filename during the move operation this is considered an "edit" and the file's modified information will be updated regardless of the "RetainEditorAndModifiedOnMove" setting.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new folder
+const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
+
+await sp.web.rootFolder.folders.getByUrl("SiteAssets").folders.getByUrl("My Folder").moveByPath(destinationUrl, true);
+
+

Added in 3.8.0

+

You can also supply a set of detailed options to better control the move process:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/folder`;
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents/folder").moveByPath(destinationUrl, {
+    KeepBoth: false,
+    RetainEditorAndModifiedOnMove: true,
+    ShouldBypassSharedLocks: false,
+});
+
+

copy by path

+

It's possible to copy a folder to a new destination within the same or a different site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new folder
+const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
+
+await sp.web.rootFolder.folders.getByUrl("SiteAssets").folders.getByUrl("My Folder").copyByPath(destinationUrl, true);
+
+

Added in 3.8.0

+

You can also supply a set of detailed options to better control the copy process:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// destination is a server-relative url of a new file
+const destinationUrl = `/sites/dev2/SiteAssets/folder`;
+
+await sp.web.getFolderByServerRelativePath("/sites/dev/Shared Documents/folder").copyByPath(destinationUrl, false, {
+    KeepBoth: false,
+    ResetAuthorAndCreatedOnCopy: true,
+    ShouldBypassSharedLocks: false,
+});
+
+

delete

+

Deletes a folder

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.rootFolder.folders.getByUrl("My Folder").delete();
+
+

delete with params

+

Deletes a folder with options

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.rootFolder.folders.getByUrl("My Folder").deleteWithParams({
+                BypassSharedLock: true,
+                DeleteIfEmpty: true,
+            });
+
+

recycle

+

Recycles a folder

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.rootFolder.folders.getByUrl("My Folder").recycle();
+
+

serverRelativeUrl

+

Gets folder's server relative url

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const relUrl = await sp.web.rootFolder.folders.getByUrl("SiteAssets").select('ServerRelativeUrl')();
+
+

update

+

Updates folder's properties

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").update({
+        "Name": "New name",
+    });
+
+

contentTypeOrder

+

Gets content type order of a folder

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const order = await sp.web.getFolderByServerRelativePath("Shared Documents").select('contentTypeOrder')();
+
+

folders

+

Gets all child folders associated with the current folder

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folders = await sp.web.rootFolder.folders();
+
+

files

+

Gets all files inside a folder

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import "@pnp/sp/files/folder";
+
+const sp = spfi(...);
+
+const files = await sp.web.getFolderByServerRelativePath("Shared Documents").files();
+
+

listItemAllFields

+

Gets this folder's list item field values

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const itemFields = await sp.web.getFolderByServerRelativePath("Shared Documents/My Folder").listItemAllFields();
+
+

parentFolder

+

Gets the parent folder, if available

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const parentFolder = await sp.web.getFolderByServerRelativePath("Shared Documents/My Folder").parentFolder();
+
+

properties

+

Gets this folder's properties

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const properties = await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").properties();
+
+

uniqueContentTypeOrder

+

Gets a value that specifies the content type order.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const contentTypeOrder = await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").select('uniqueContentTypeOrder')();
+
+

Rename a folder

+

You can rename a folder by updating FileLeafRef property:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder = sp.web.getFolderByServerRelativePath("Shared Documents/My Folder");
+
+const item = await folder.getItem();
+const result = await item.update({ FileLeafRef: "Folder2" });
+
+

Create a folder with custom content type

+

Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/items";
+import "@pnp/sp/folders";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+const newFolderResult = await sp.web.rootFolder.folders.getByUrl("Shared Documents").folders.addUsingPath("My New Folder");
+const item = await newFolderResult.folder.listItemAllFields();
+
+await sp.web.lists.getByTitle("Documents").items.getById(item.ID).update({
+    ContentTypeId: "0x0120001E76ED75A3E3F3408811F0BF56C4CDDD",
+    MyFolderField: "field value",
+    Title: "My New Folder",
+});
+
+

addSubFolderUsingPath

+

You can use the addSubFolderUsingPath method to add a folder with some special chars supported

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import { IFolder } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+// add a folder to site assets
+const folder: IFolder = await sp.web.rootFolder.folders.getByUrl("SiteAssets").addSubFolderUsingPath("folder name");
+
+

getFolderById

+

You can get a folder by Id from a web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+import { IFolder } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
+
+

getParentInfos

+

Gets information about folder, including details about the parent list, parent list root folder, and parent web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
+await folder.getParentInfos();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/forms/index.html b/sp/forms/index.html new file mode 100644 index 000000000..50e528f3d --- /dev/null +++ b/sp/forms/index.html @@ -0,0 +1,2702 @@ + + + + + + + + + + + + + + + + + + + + + + + + Forms - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/forms

+

Forms in SharePoint are the Display, New, and Edit forms associated with a list.

+

IForms

+

Invokable Banner Selective Imports Banner

+

Get Form by Id

+

Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/forms";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// get the field by Id for web
+const form = sp.web.lists.getByTitle("Documents").forms.getById("{c4486774-f1e2-4804-96f3-91edf3e22a19}")();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/groupSiteManager/index.html b/sp/groupSiteManager/index.html new file mode 100644 index 000000000..aabcf9465 --- /dev/null +++ b/sp/groupSiteManager/index.html @@ -0,0 +1,2694 @@ + + + + + + + + + + + + + + + + + + + + + + + + GroupSiteManager - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

GroupSiteManager

+ +

@pnp/sp/groupsitemanager

+

The @pnp/sp/groupsitemanager package represents calls to _api/groupsitemanager endpoint and is accessible from any site url.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/groupsitemanager";
+
+const sp = spfi(...);
+
+// call method to check if the current user can create Microsoft 365 groups
+const isUserAllowed = await sp.groupSiteManager.canUserCreateGroup();
+
+// call method to delete a group-connected site
+await sp.groupSiteManager.delete("https://contoso.sharepoint.com/sites/hrteam");
+
+//call method to gets labels configured for the tenant
+const orgLabels = await sp.groupSiteManager.getAllOrgLabels(0);
+
+//call method to get information regarding site groupification configuration for the current site context
+const groupCreationContext = await sp.groupSiteManager.getGroupCreationContext();
+
+//call method to get information regarding site groupification configuration for the current site context
+const siteData = await sp.groupSiteManager.getGroupSiteConversionData();
+
+// call method to get teams membership for a user
+const userTeams = await sp.groupSiteManager.getUserTeamConnectedMemberGroups("meganb@contoso.onmicrosoft.com");
+
+// call method to get shared channel memberhsip for user
+const sharedChannels = await sp.groupSiteManager.getUserSharedChannelMemberGroups("meganb@contoso.onmicrosoft.com");
+
+//call method to get valid site url from Alias
+const siteUrl = await sp.groupSiteManager.getValidSiteUrlFromAlias("contoso");
+
+//call method to check if teamify prompt is hidden
+const isTeamifyPromptHidden = await sp.groupSiteManager.isTeamifyPromptHidden("https://contoso.sharepoint.com/sites/hrteam");
+
+
+

For more information on the methods available and how to use them, please review the code comments in the source.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/hubsites/index.html b/sp/hubsites/index.html new file mode 100644 index 000000000..88728e7f2 --- /dev/null +++ b/sp/hubsites/index.html @@ -0,0 +1,2916 @@ + + + + + + + + + + + + + + + + + + + + + + + + Hubsites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/hubsites

+

This module helps you with working with hub sites in your tenant.

+

IHubSites

+

Invokable Banner Selective Imports Banner

+

Get a Listing of All Hub sites

+
import { spfi } from "@pnp/sp";
+import { IHubSiteInfo } from  "@pnp/sp/hubsites";
+import "@pnp/sp/hubsites";
+
+const sp = spfi(...);
+
+// invoke the hub sites object
+const hubsites: IHubSiteInfo[] = await sp.hubSites();
+
+// you can also use select to only return certain fields:
+const hubsites2: IHubSiteInfo[] = await sp.hubSites.select("ID", "Title", "RelatedHubSiteIds")();
+
+

Get Hub site by Id

+

Using the getById method on the hubsites module to get a hub site by site Id (guid).

+
import { spfi } from "@pnp/sp";
+import { IHubSiteInfo } from  "@pnp/sp/hubsites";
+import "@pnp/sp/hubsites";
+
+const sp = spfi(...);
+
+const hubsite: IHubSiteInfo = await sp.hubSites.getById("3504348e-b2be-49fb-a2a9-2d748db64beb")();
+
+// log hub site title to console
+console.log(hubsite.Title);
+
+

Get ISite instance

+

We provide a helper method to load the ISite instance from the HubSite

+
import { spfi } from "@pnp/sp";
+import { ISite } from  "@pnp/sp/sites";
+import "@pnp/sp/hubsites";
+
+const sp = spfi(...);
+
+const site: ISite = await sp.hubSites.getById("3504348e-b2be-49fb-a2a9-2d748db64beb").getSite();
+
+const siteData = await site();
+
+console.log(siteData.Title);
+
+

Get Hub site data for a web

+
import { spfi } from "@pnp/sp";
+import { IHubSiteWebData } from  "@pnp/sp/hubsites";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+
+const webData: Partial<IHubSiteWebData> = await sp.web.hubSiteData();
+
+// you can also force a refresh of the hub site data
+const webData2: Partial<IHubSiteWebData> = await sp.web.hubSiteData(true);
+
+

syncHubSiteTheme

+

Allows you to apply theme updates from the parent hub site collection.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+
+await sp.web.syncHubSiteTheme();
+
+

Hub site Site Methods

+

You manage hub sites at the Site level.

+

joinHubSite

+

Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/hubsites/site";
+
+const sp = spfi(...);
+
+// join a site to a hub site
+await sp.site.joinHubSite("{parent hub site id}");
+
+// remove a site from a hub site
+await sp.site.joinHubSite("00000000-0000-0000-0000-000000000000");
+
+

registerHubSite

+

Registers the current site collection as hub site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/hubsites/site";
+
+const sp = spfi(...);
+
+// register current site as a hub site
+await sp.site.registerHubSite();
+
+

unRegisterHubSite

+

Un-registers the current site collection as hub site collection.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/hubsites/site";
+
+const sp = spfi(...);
+
+// make a site no longer a hub
+await sp.site.unRegisterHubSite();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/items/index.html b/sp/items/index.html new file mode 100644 index 000000000..7e3b6bed1 --- /dev/null +++ b/sp/items/index.html @@ -0,0 +1,3537 @@ + + + + + + + + + + + + + + + + + + + + + + + + List Items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/items

+

Invokable Banner Selective Imports Banner

+

GET

+

Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.

+

Basic Get

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// get all the items from a list
+const items: any[] = await sp.web.lists.getByTitle("My List").items();
+console.log(items);
+
+// get a specific item by id.
+const item: any = await sp.web.lists.getByTitle("My List").items.getById(1)();
+console.log(item);
+
+// use odata operators for more efficient queries
+const items2: any[] = await sp.web.lists.getByTitle("My List").items.select("Title", "Description").top(5).orderBy("Modified", true)();
+console.log(items2);
+
+

Get Paged Items

+

Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// basic case to get paged items form a list
+const items = await sp.web.lists.getByTitle("BigList").items.getPaged();
+
+// you can also provide a type for the returned values instead of any
+const items = await sp.web.lists.getByTitle("BigList").items.getPaged<{Title: string}[]>();
+
+// the query also works with select to choose certain fields and top to set the page size
+const items = await sp.web.lists.getByTitle("BigList").items.select("Title", "Description").top(50).getPaged<{Title: string}[]>();
+
+// the results object will have two properties and one method:
+
+// the results property will be an array of the items returned
+if (items.results.length > 0) {
+    console.log("We got results!");
+
+    for (let i = 0; i < items.results.length; i++) {
+        // type checking works here if we specify the return type
+        console.log(items.results[i].Title);
+    }
+}
+
+// the hasNext property is used with the getNext method to handle paging
+// hasNext will be true so long as there are additional results
+if (items.hasNext) {
+
+    // this will carry over the type specified in the original query for the results array
+    items = await items.getNext();
+    console.log(items.results.length);
+}
+
+

getListItemChangesSinceToken

+

The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// Using RowLimit. Enables paging
+const changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({RowLimit: '5'});
+
+// Use QueryOptions to make a XML-style query.
+// Because it's XML we need to escape special characters
+// Instead of & we use &amp; in the query
+const changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext="Paged=TRUE&amp;p_ID=5" />'});
+
+// Get everything. Using null with ChangeToken gets everything
+const changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({ChangeToken: null});
+
+
+

Get All Items

+

Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used.

+
+

In v3 there is a separate import for get-all to include the functionality. This is to remove the code from bundles for folks who do not need it.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/items/get-all";
+
+const sp = spfi(...);
+
+// basic usage
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.getAll();
+console.log(allItems.length);
+
+// set page size
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.getAll(4000);
+console.log(allItems.length);
+
+// use select and top. top will set page size and override the any value passed to getAll
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.select("Title").top(4000).getAll();
+console.log(allItems.length);
+
+// we can also use filter as a supported odata operation, but this will likely fail on large lists
+const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.select("Title").filter("Title eq 'Test'").getAll();
+console.log(allItems.length);
+
+

Retrieving Lookup Fields

+

When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const items = await sp.web.lists.getByTitle("LookupList").items.select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup")();
+console.log(items);
+
+const item = await sp.web.lists.getByTitle("LookupList").items.getById(1).select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup")();
+console.log(item);
+
+

Filter using Metadata fields

+

To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+
+const sp = spfi(...);
+
+const r = await sp.web.lists.getByTitle("TaxonomyList").getItemsByCAMLQuery({
+    ViewXml: `<View><Query><Where><Eq><FieldRef Name="MetaData"/><Value Type="TaxonomyFieldType">Term 2</Value></Eq></Where></Query></View>`,
+});
+
+

Retrieving PublishingPageImage

+

The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { Web } from "@pnp/sp/webs";
+
+try {
+  const sp = spfi("https://{publishing site url}").using(SPFx(this.context));
+
+  const r = await sp.web.lists.getByTitle("Pages").items
+    .select("Title", "FileRef", "FieldValuesAsText/MetaInfo")
+    .expand("FieldValuesAsText")
+    ();
+
+  // look through the returned items.
+  for (var i = 0; i < r.length; i++) {
+
+    // the title field value
+    console.log(r[i].Title);
+
+    // find the value in the MetaInfo string using regex
+    const matches = /PublishingPageImage:SW\|(.*?)\r\n/ig.exec(r[i].FieldValuesAsText.MetaInfo);
+    if (matches !== null && matches.length > 1) {
+
+      // this wil be the value of the PublishingPageImage field
+      console.log(matches[1]);
+    }
+  }
+}
+catch (e) {
+  console.error(e);
+}
+
+

Add Items

+

There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { IItemAddResult } from "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// add an item to the list
+const iar: IItemAddResult = await sp.web.lists.getByTitle("My List").items.add({
+  Title: "Title",
+  Description: "Description"
+});
+
+console.log(iar);
+
+

Content Type

+

You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+await sp.web.lists.getById("4D5A36EA-6E84-4160-8458-65C436DB765C").items.add({
+    Title: "Test 1",
+    ContentTypeId: "0x01030058FD86C279252341AB303852303E4DAF"
+});
+
+

User Fields

+

There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with "Id" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.

+

Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an array. Examples for both are shown below.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { getGUID } from "@pnp/core";
+
+const sp = spfi(...);
+
+const i = await sp.web.lists.getByTitle("PeopleFields").items.add({
+  Title: getGUID(),
+  User1Id: 9, // allows a single user
+  User2Id: [16, 45] // allows multiple users
+});
+
+console.log(i);
+
+

If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const result = await sp.web.lists.getByTitle("UserFieldList").items.getById(1).validateUpdateListItem([{
+    FieldName: "UserField",
+    FieldValue: JSON.stringify([{ "Key": "i:0#.f|membership|person@tenant.com" }]),
+},
+{
+    FieldName: "Title",
+    FieldValue: "Test - Updated",
+}]);
+
+

Lookup Fields

+

What is said for User Fields is, in general, relevant to Lookup Fields:

+
    +
  • Lookup Field types:
  • +
  • Single-valued lookup
  • +
  • Multiple-valued lookup
  • +
  • Id suffix should be appended to the end of lookups EntityPropertyName in payloads
  • +
  • Numeric Ids for lookups' items should be passed as values
  • +
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import { getGUID } from "@pnp/core";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("LookupFields").items.add({
+    Title: getGUID(),
+    LookupFieldId: 2,       // allows a single lookup value
+    MultiLookupFieldId: [1, 56]  // allows multiple lookup value
+});
+
+

Add Multiple Items

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/batching";
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+const list = batchedSP.web.lists.getByTitle("rapidadd");
+
+let res = [];
+
+list.items.add({ Title: "Batch 6" }).then(r => res.push(r));
+
+list.items.add({ Title: "Batch 7" }).then(r => res.push(r));
+
+// Executes the batched calls
+await execute();
+
+// Results for all batched calls are available
+for(let i = 0; i < res.length; i++) {
+    ///Do something with the results
+}
+
+

Update Items

+

The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.

+
+

Note: For updating certain types of fields, see the Add examples above. The payload will be the same you will just need to replace the .add method with .getById({itemId}).update.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+const i = await list.items.getById(1).update({
+  Title: "My New Title",
+  Description: "Here is a new description"
+});
+
+console.log(i);
+
+

Getting and updating a collection using filter

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// you are getting back a collection here
+const items: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'")();
+
+// see if we got something
+if (items.length > 0) {
+  const updatedItem = await sp.web.lists.getByTitle("MyList").items.getById(items[0].Id).update({
+    Title: "Updated Title",
+  });
+
+  console.log(JSON.stringify(updatedItem));
+}
+
+

Update Multiple Items

+

This approach avoids multiple calls for the same list's entity type name.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/batching"
+
+const sp = spfi(...);
+
+const [batchedSP, execute] = sp.batched();
+
+const list = batchedSP.web.lists.getByTitle("rapidupdate");
+
+list.items.getById(1).update({ Title: "Batch 6" }).then(b => {
+  console.log(b);
+});
+
+list.items.getById(2).update({ Title: "Batch 7" }).then(b => {
+  console.log(b);
+});
+
+// Executes the batched calls
+await execute();
+
+console.log("Done");
+
+

Update Taxonomy field

+

Note: Updating Taxonomy field for a File item should be handled differently. Instead of using update(), use validateUpdateListItem(). Please see below

+

List Item

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("Demo").items.getById(1).update({
+    MetaDataColumn: { Label: "Demo", TermGuid: '883e4c81-e8f9-4f19-b90b-6ab805c9f626', WssId: '-1' }
+});
+
+
+

File List Item

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+await (await sp.web.getFileByServerRelativePath("/sites/demo/DemoLibrary/File.txt").getItem()).validateUpdateListItem([{
+    FieldName: "MetaDataColumn",
+    FieldValue:"Demo|883e4c81-e8f9-4f19-b90b-6ab805c9f626", //Label|TermGuid
+}]);
+
+

Update Multi-value Taxonomy field

+

Based on this excellent article from Beau Cameron.

+

As he says you must update a hidden field to get this to work via REST. My meta data field accepting multiple values is called "MultiMetaData".

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+// first we need to get the hidden field's internal name.
+// The Title of that hidden field is, in my case and in the linked article just the visible field name with "_0" appended.
+const fields = await sp.web.lists.getByTitle("TestList").fields.filter("Title eq 'MultiMetaData_0'").select("Title", "InternalName")();
+// get an item to update, here we just create one for testing
+const newItem = await sp.web.lists.getByTitle("TestList").items.add({
+  Title: "Testing",
+});
+// now we have to create an update object
+// to do that for each field value you need to serialize each as -1;#{field label}|{field id} joined by ";#"
+// update with the values you want, this also works in the add call directly to avoid a second call
+const updateVal = {};
+updateVal[fields[0].InternalName] = "-1;#New Term|bb046161-49cc-41bd-a459-5667175920d4;#-1;#New 2|0069972e-67f1-4c5e-99b6-24ac5c90b7c9";
+// execute the update call
+await newItem.item.update(updateVal);
+
+

Update BCS Field

+

Please see the issue for full details.

+

You will need to use validateUpdateListItem to ensure hte BCS field is updated correctly.

+
const update = await sp.web.lists.getByTitle("Price").items.getById(7).select('*,External').validateUpdateListItem([
+      {FieldName:"External",FieldValue:"Fauntleroy Circus"},
+      {FieldName:"Customers_ID", FieldValue:"__bk410024003500240054006500"}
+    ]); 
+
+

Recycle

+

To send an item to the recycle bin use recycle.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+const recycleBinIdentifier = await list.items.getById(1).recycle();
+
+

Delete

+

Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+await list.items.getById(1).delete();
+
+

Delete With Params

+

Deletes the item object with options.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("MyList");
+
+await list.items.getById(1).deleteWithParams({
+                BypassSharedLock: true,
+            });
+
+
+

The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true

+
+

Resolving field names

+

It's a very common mistake trying wrong field names in the requests. +Field's EntityPropertyName value should be used.

+

The easiest way to get know EntityPropertyName is to use the following snippet:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/fields";
+
+const sp = spfi(...);
+
+const response =
+  await sp.web.lists
+    .getByTitle('[Lists_Title]')
+    .fields
+    .select('Title, EntityPropertyName')
+    .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`)
+    ();
+
+console.log(response.map(field => {
+  return {
+    Title: field.Title,
+    EntityPropertyName: field.EntityPropertyName
+  };
+}));
+
+

Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.

+

getParentInfos

+

Gets information about an item, including details about the parent list, parent list root folder, and parent web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+const item: any = await sp.web.lists.getByTitle("My List").items.getById(1)();
+await item.getParentInfos();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/lists/index.html b/sp/lists/index.html new file mode 100644 index 000000000..3f293fc93 --- /dev/null +++ b/sp/lists/index.html @@ -0,0 +1,3608 @@ + + + + + + + + + + + + + + + + + + + + + + + + Lists - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/lists

+

Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.

+

ILists

+

Invokable Banner Selective Imports Banner

+

Get List by Id

+

Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// get the list by Id
+const list = sp.web.lists.getById("03b05ff4-d95d-45ed-841d-3855f77a2483");
+
+// we can use this 'list' variable to execute more queries on the list:
+const r = await list.select("Title")();
+
+// show the response from the server
+console.log(r.Title);
+
+

Get List by Title

+

You can also get a list from the collection by title.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// get the default document library 'Documents'
+const list = sp.web.lists.getByTitle("Documents");
+
+// we can use this 'list' variable to run more queries on the list:
+const r = await list.select("Id")();
+
+// log the list Id to console
+console.log(r.Id);
+
+

Add List

+

You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+// create a new list, passing only the title
+const listAddResult = await sp.web.lists.add("My new list");
+
+// we can work with the list created using the IListAddResult.list property:
+const r = await listAddResult.list.select("Title")();
+
+// log newly created list title to console
+console.log(r.Title);
+});
+
+

You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs.

+
// this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings)
+const listAddResult = await sp.web.lists.add("My Doc Library", "This is a description of doc lib.", 101, true, { OnQuickLaunch: true });
+
+// get the Id of the newly added document library
+const r = await listAddResult.list.select("Id")();
+
+// log id to console
+console.log(r.Id);
+
+

Ensure that a List exists (by title)

+

Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings.

+

Batching Not Supported Banner

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default):
+const listEnsureResult = await sp.web.lists.ensure("My List");
+
+// check if the list was created, or if it already existed:
+if (listEnsureResult.created) {
+    console.log("My List was created!");
+} else {
+    console.log("My List already existed!");
+}
+
+// work on the created/updated list
+const r = await listEnsureResult.list.select("Id")();
+
+// log the Id
+console.log(r.Id);
+
+

If the list already exists, the other settings you provide will be used to update the existing list.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// add a new list to the lists collection of the web
+sp.web.lists.add("My List 2").then(async () => {
+
+// then call ensure on the created list with an updated description
+const listEnsureResult = await sp.web.lists.ensure("My List 2", "Updated description");
+
+// get the updated description
+const r = await listEnsureResult.list.select("Description")();
+
+// log the updated description
+console.log(r.Description);
+});
+
+

Ensure Site Assets Library exist

+

Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// get Site Assets library
+const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary();
+
+// get the Title
+const r = await siteAssetsList.select("Title")();
+
+// log Title
+console.log(r.Title);
+
+

Ensure Site Pages Library exist

+

Gets a list that is the default location for wiki pages.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+// get Site Pages library
+const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary();
+
+// get the Title
+const r = await siteAssetsList.select("Title")();
+
+// log Title
+console.log(r.Title);
+
+

IList

+

Invokable Banner Selective Imports Banner

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ScenarioImport Statement
Selective 1import { List, IList } from "@pnp/sp/lists";
Selective 2import "@pnp/sp/lists";
Preset: Allimport { sp, List, IList } from "@pnp/sp/presets/all";
Preset: Coreimport { sp, List, IList } from "@pnp/sp/presets/core";
+

Update a list

+

Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is "*")

+
import { IListUpdateResult } from "@pnp/sp/lists";
+
+// create a TypedHash object with the properties to update
+const updateProperties = {
+    Description: "This list title and description has been updated using PnPjs.",
+    Title: "Updated title",
+};
+
+// update the list with the properties above
+list.update(updateProperties).then(async (l: IListUpdateResult) => {
+
+    // get the updated title and description
+    const r = await l.list.select("Title", "Description")();
+
+    // log the updated properties to the console
+    console.log(r.Title);
+    console.log(r.Description);
+});
+
+

Get changes on a list

+

From the change log, you can get a collection of changes that have occurred within the list based on the specified query.

+
import { IChangeQuery } from "@pnp/sp";
+
+// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore
+const changeQuery: IChangeQuery = {
+    Add: true,
+    ChangeTokenEnd: null,
+    ChangeTokenStart: null,
+    DeleteObject: true,
+    Rename: true,
+    Restore: true,
+};
+
+// get list changes
+const r = await list.getChanges(changeQuery);
+
+// log changes to console
+console.log(r);
+
+

To get changes from a specific time range you can use the ChangeTokenStart or a combination of ChangeTokenStart and ChangeTokenEnd.

+
import { IChangeQuery } from "@pnp/sp";
+
+//Resource is the list Id (as Guid)
+const resource = list.Id;
+const changeStart = new Date("2022-02-22").getTime();
+const changeTokenStart = `1;3;${resource};${changeStart};-1`;
+
+// build the changeQuery object, here we look at changes regarding Add and Update for Items.
+const changeQuery: IChangeQuery = {
+    Add: true,
+    Update: true,
+    Item: true,
+    ChangeTokenEnd: null,
+    ChangeTokenStart: { StringValue: changeTokenStart },
+};
+
+// get list changes
+const r = await list.getChanges(changeQuery);
+
+// log changes to console
+console.log(r);
+
+

Get list items using a CAML Query

+

You can get items from SharePoint using a CAML Query.

+
import { ICamlQuery } from "@pnp/sp/lists";
+
+// build the caml query object (in this example, we include Title field and limit rows to 5)
+const caml: ICamlQuery = {
+    ViewXml: "<View><ViewFields><FieldRef Name='Title' /></ViewFields><RowLimit>5</RowLimit></View>",
+};
+
+// get list items
+const r = await list.getItemsByCAMLQuery(caml);
+
+// log resulting array to console
+console.log(r);
+
+

If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment)

+
import { ICamlQuery } from "@pnp/sp/lists";
+
+// build the caml query object (in this example, we include Title field and limit rows to 5)
+const caml: ICamlQuery = {
+    ViewXml: "<View><ViewFields><FieldRef Name='Title' /><FieldRef Name='RoleAssignments' /></ViewFields><RowLimit>5</RowLimit></View>",
+};
+
+// get list items
+const r = await list.getItemsByCAMLQuery(caml, "RoleAssignments");
+
+// log resulting item array to console
+console.log(r);
+
+

Get list items changes using a Token

+
import {  IChangeLogItemQuery } from "@pnp/sp/lists";
+
+// build the caml query object (in this example, we include Title field and limit rows to 5)
+const changeLogItemQuery: IChangeLogItemQuery = {
+    Contains: `<Contains><FieldRef Name="Title"/><Value Type="Text">Item16</Value></Contains>`,
+    QueryOptions: `<QueryOptions>
+    <IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns>
+    <DateInUtc>False</DateInUtc>
+    <IncludePermissions>TRUE</IncludePermissions>
+    <IncludeAttachmentUrls>FALSE</IncludeAttachmentUrls>
+    <Folder>My List</Folder></QueryOptions>`,
+};
+
+// get list items
+const r = await list.getListItemChangesSinceToken(changeLogItemQuery);
+
+// log resulting XML to console
+console.log(r);
+
+

Recycle a list

+

Removes the list from the web's list collection and puts it in the recycle bin.

+
await list.recycle();
+
+

Render list data

+
import { IRenderListData } from "@pnp/sp/lists";
+
+// render list data, top 5 items
+const r: IRenderListData = await list.renderListData("<View><RowLimit>5</RowLimit></View>");
+
+// log array of items in response
+console.log(r.Row);
+
+

Render list data as stream

+
import { IRenderListDataParameters } from "@pnp/sp/lists";
+// setup parameters object
+const renderListDataParams: IRenderListDataParameters = {
+    ViewXml: "<View><RowLimit>5</RowLimit></View>",
+};
+// render list data as stream
+const r = await list.renderListDataAsStream(renderListDataParams);
+// log array of items in response
+console.log(r.Row);
+
+

You can also supply other options to renderListDataAsStream including override parameters and query params. This can be helpful when looking to apply sorting to the returned data.

+
import { IRenderListDataParameters } from "@pnp/sp/lists";
+// setup parameters object
+const renderListDataParams: IRenderListDataParameters = {
+    ViewXml: "<View><RowLimit>5</RowLimit></View>",
+};
+const overrideParams = {
+    ViewId = "{view guid}"
+};
+// OR if you don't want to supply override params use null
+// overrideParams = null;
+// Set the query params using a map
+const query = new Map<string, string>();
+query.set("SortField", "{AField}");
+query.set("SortDir", "Desc");
+// render list data as stream
+const r = await list.renderListDataAsStream(renderListDataParams, overrideParams, query);
+// log array of items in response
+console.log(r.Row);
+
+

Reserve list item Id for idempotent list item creation

+
const listItemId = await list.reserveListItemId();
+
+// log id to console
+console.log(listItemId);
+
+

Add a list item using path (folder), validation and set field values

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+const list = await sp.webs.lists.getByTitle("MyList").select("Title", "ParentWebUrl")();
+const formValues: IListItemFormUpdateValue[] = [
+                {
+                    FieldName: "Title",
+                    FieldValue: title,
+                },
+            ];
+
+list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)
+
+
+

content-types imports

+

contentTypes

+

Get all content types for a list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+import "@pnp/sp/content-types/list";
+
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.contentTypes();
+
+

fields imports

+ + + + + + + + + + + + + + + + + + + + + +
ScenarioImport Statement
Selective 1import "@pnp/sp/fields";
Selective 2import "@pnp/sp/fields/list";
Preset: Allimport { sp } from "@pnp/sp/presets/all";
+

fields

+

Get all the fields for a list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+import "@pnp/sp/fields/list";
+
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.fields();
+
+

Add a field to the site, then add the site field to a list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+const fld = await sp.site.rootWeb.fields.addText("MyField");
+await sp.web.lists.getByTitle("MyList").fields.createFieldAsXml(fld.data.SchemaXml);
+
+

folders

+

Get the root folder of a list.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/folders/list";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.rootFolder();
+
+

forms

+
import "@pnp/sp/forms/list";
+
+const r = await list.forms();
+
+

items

+

Get a collection of list items.

+
import "@pnp/sp/items/list";
+
+const r = await list.items();
+
+

views

+

Get the default view of the list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views/list";
+
+const sp = spfi(...);
+const list = sp.web.lists.getByTitle("Documents");
+const views = await list.views();
+const defaultView = await list.defaultView();
+
+

Get a list view by Id

+
const view = await list.getView(defaultView.Id).select("Title")();
+
+

security imports

+

To work with list security, you can import the list methods as follows:

+
import "@pnp/sp/security/list";
+
+

For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.

+

subscriptions

+

Get all subscriptions on the list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/subscriptions/list";
+
+const sp = spfi(...);
+const list = sp.web.lists.getByTitle("Documents");
+const subscriptions = await list.subscriptions();
+
+

userCustomActions

+

Get a collection of the list's user custom actions.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/user-custom-actions/web"
+
+const sp = spfi(...);
+const list = sp.web.lists.getByTitle("Documents");
+const r = await list.userCustomActions();
+
+

getParentInfos

+

Gets information about an list, including details about the parent list root folder, and parent web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("Documents");
+await list.getParentInfos();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/navigation/index.html b/sp/navigation/index.html new file mode 100644 index 000000000..e510ee33b --- /dev/null +++ b/sp/navigation/index.html @@ -0,0 +1,2964 @@ + + + + + + + + + + + + + + + + + + + + + + + + Navigation - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp - navigation

+

Selective Imports Banner

+ +

getMenuState

+

The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.

+

The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\,containingcomma

+

NOTE: the , separator can be escaped using the \ as escape character as done in the example above. The string above would split like:

+
    +
  • property1
  • +
  • property2
  • +
  • property3,containingcomma
  • +
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+
+// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels.
+const state = await sp.navigation.getMenuState();
+
+// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5
+const state2 = await sp.navigation.getMenuState("1002", 5);
+
+// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5
+const state3 = await sp.navigation.getMenuState(null, 5, "CurrentNavSiteMapProviderNoEncode");
+
+

getMenuNodeKey

+

Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+
+const key = await sp.navigation.getMenuNodeKey("/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx");
+
+

Web Navigation

+

Invokable Banner Selective Imports Banner

+ + + + + + + + + + + + + +
ScenarioImport Statement
Selective 1import "@pnp/sp/webs";
import "@pnp/sp/navigation";
+

The navigation object contains two properties "quicklaunch" and "topnavigationbar". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.

+

Get navigation

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+
+const top = await sp.web.navigation.topNavigationBar();
+const quick = await sp.web.navigation.quicklaunch();
+
+

For the following examples we will refer to a variable named "nav" that is understood to be one of topNavigationBar or quicklaunch:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation";
+
+const sp = spfi(...);
+// note we are just getting a ref to the nav object, not executing a request
+const nav = sp.web.navigation.topNavigationBar;
+// -- OR -- 
+// note we are just getting a ref to the nav object, not executing a request
+const nav = sp.web.navigation.quicklaunch;
+
+

getById

+
import "@pnp/sp/navigation";
+
+const node = await nav.getById(3)();
+
+

add

+
import "@pnp/sp/navigation";
+
+const result = await nav.add("Node Title", "/sites/dev/pages/mypage.aspx", true);
+
+const nodeDataRaw = result.data;
+
+// request the data from the created node
+const nodeData = result.node();
+
+

moveAfter

+

Places a navigation node after another node in the tree

+
import "@pnp/sp/navigation";
+
+const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true);
+const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true);
+const node1 = await node1result.node();
+const node2 = await node2result.node();
+
+await nav.moveAfter(node1.Id, node2.Id);
+
+

Delete

+

Deletes a given node

+
import "@pnp/sp/navigation";
+
+const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true);
+let nodes = await nav();
+// check we added a node
+let index = nodes.findIndex(n => n.Id === node1result.data.Id)
+// index >= 0
+
+// delete a node
+await nav.getById(node1result.data.Id).delete();
+
+nodes = await nav();
+index = nodes.findIndex(n => n.Id === node1result.data.Id)
+// index = -1
+
+

Update

+

You are able to update various properties of a given node, such as the the Title, Url, IsVisible.

+

You may update the Audience Targeting value for the node by passing in Microsoft Group IDs in the AudienceIds array. Be aware, Audience Targeting must already be enabled on the navigation.

+
import "@pnp/sp/navigation";
+
+
+await nav.getById(4).update({
+    Title: "A new title",
+    AudienceIds:["d50f9511-b811-4d76-b20a-0d6e1c8095f7"],
+    Url:"/sites/dev/SitePages/home.aspx",
+    IsVisible:false
+});
+
+

Children

+

The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch.

+
import "@pnp/sp/navigation";
+
+const childrenData = await nav.getById(1).children();
+
+// add a child
+await nav.getById(1).children.add("Title", "Url", true);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/permissions/index.html b/sp/permissions/index.html new file mode 100644 index 000000000..8b9f161e3 --- /dev/null +++ b/sp/permissions/index.html @@ -0,0 +1,2777 @@ + + + + + + + + + + + + + + + + + + + + + + + + Permissions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp - permissions

+

A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.

+

Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.

+

Get Role Assignments

+

This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const roles = await sp.web.roleAssignments();
+
+

First Unique Ancestor Securable Object

+

This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const obj = await sp.web.firstUniqueAncestorSecurableObject();
+
+

User Effective Permissions

+

This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const perms = await sp.web.getUserEffectivePermissions("i:0#.f|membership|user@site.com");
+
+const perms2 = await sp.web.getCurrentUserEffectivePermissions();
+
+

User Has Permissions

+

Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const perms = await sp.web.userHasPermissions("i:0#.f|membership|user@site.com", PermissionKind.ApproveItems);
+
+const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems);
+
+

Has Permissions

+

If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+const perms = await sp.web.getCurrentUserEffectivePermissions();
+if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) {
+    // ...
+}
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/profiles/index.html b/sp/profiles/index.html new file mode 100644 index 000000000..5eb91deb0 --- /dev/null +++ b/sp/profiles/index.html @@ -0,0 +1,3259 @@ + + + + + + + + + + + + + + + + + + + + + + + + Profiles - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/profiles

+

The profile services allows you to work with the SharePoint User Profile Store.

+

Profiles

+

Profiles is accessed directly from the root sp object.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/profiles";
+
+ +
getEditProfileLink(): Promise<string>
+
+
const sp = spfi(...);
+const editProfileLink = await sp.profiles.getEditProfileLink();
+
+

Is My People List Public

+

Provides a boolean that indicates if the current users "People I'm Following" list is public or not

+
getIsMyPeopleListPublic(): Promise<boolean>
+
+
const sp = spfi(...);
+const isPublic = await sp.profiles.getIsMyPeopleListPublic();
+
+

Find out if the current user is followed by another user

+

Provides a boolean that indicates if the current users is followed by a specific user.

+
amIFollowedBy(loginName: string): Promise<boolean>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const isFollowedBy = await sp.profiles.amIFollowedBy(loginName);
+
+

Find out if I am following a specific user

+

Provides a boolean that indicates if the current users is followed by a specific user.

+
amIFollowing(loginName: string): Promise<boolean>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const following = await sp.profiles.amIFollowing(loginName);
+
+

Get the tags I follow

+

Gets the tags the current user is following. Accepts max count, default is 20.

+
getFollowedTags(maxCount = 20): Promise<string[]>
+
+
const sp = spfi(...);
+const tags = await sp.profiles.getFollowedTags();
+
+

Get followers for a specific user

+

Gets the people who are following the specified user.

+
getFollowersFor(loginName: string): Promise<any[]>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const followers = await sp.profiles.getFollowersFor(loginName);
+followers.forEach((value) => {
+  ...
+});
+
+

Get followers for the current

+

Gets the people who are following the current user.

+
myFollowers(): ISPCollection
+
+
const sp = spfi(...);
+const folowers = await sp.profiles.myFollowers();
+
+

Get the properties for the current user

+

Gets user properties for the current user.

+
myProperties(): ISPInstance
+
+
const sp = spfi(...);
+const profile = await sp.profiles.myProperties();
+console.log(profile.DisplayName);
+console.log(profile.Email);
+console.log(profile.Title);
+console.log(profile.UserProfileProperties.length);
+
+// Properties are stored in Key/Value pairs,
+// so parse into an object called userProperties
+var props = {};
+profile.UserProfileProperties.forEach((prop) => {
+  props[prop.Key] = prop.Value;
+});
+profile.userProperties = props;
+console.log("Account Name: " + profile.userProperties.AccountName);
+
+
// you can also select properties to return before
+const sp = spfi(...);
+const profile = await sp.profiles.myProperties.select("Title", "Email")();
+console.log(profile.Email);
+console.log(profile.Title);
+
+

Gets people specified user is following

+
getPeopleFollowedBy(loginName: string): Promise<any[]>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const folowers = await sp.profiles.getFollowersFor(loginName);
+followers.forEach((value) => {
+  ...
+});
+
+

Gets properties for a specified user

+
getPropertiesFor(loginName: string): Promise<any>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const profile = await sp.profiles.getPropertiesFor(loginName);
+console.log(profile.DisplayName);
+console.log(profile.Email);
+console.log(profile.Title);
+console.log(profile.UserProfileProperties.length);
+
+// Properties are stored in inconvenient Key/Value pairs,
+// so parse into an object called userProperties
+var props = {};
+profile.UserProfileProperties.forEach((prop) => {
+  props[prop.Key] = prop.Value;
+});
+
+profile.userProperties = props;
+console.log("Account Name: " + profile.userProperties.AccountName);
+
+ +

Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first

+
trendingTags(): Promise<IHashTagCollection>
+
+
const sp = spfi(...);
+const tags = await sp.profiles.trendingTags();
+tags.Items.forEach((tag) => {
+  ...
+});
+
+

Gets specified user profile property for the specified user

+
getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const propertyName = "AccountName";
+const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);
+
+

Hide specific user from list of suggested people

+

Removes the specified user from the user's list of suggested people to follow.

+
hideSuggestion(loginName: string): Promise<void>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+await sp.profiles.hideSuggestion(loginName);
+
+

Is one user following another

+

Indicates whether the first user is following the second user. +First parameter is the account name of the user who might be following the followee. +Second parameter is the account name of the user who might be followed by the follower.

+
isFollowing(follower: string, followee: string): Promise<boolean>
+
+
const sp = spfi(...);
+const follower = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const followee = "i:0#.f|membership|testuser2@mytenant.onmicrosoft.com";
+const isFollowing = await sp.profiles.isFollowing(follower, followee);
+
+

Set User Profile Picture

+

Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. +Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB.

+

Batching Not Supported Banner

+
setMyProfilePic(profilePicSource: Blob): Promise<void>
+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/profiles";
+import "@pnp/sp/folders";
+import "@pnp/sp/files";
+
+const sp = spfi(...);
+
+// get the blob object through a request or from a file input
+const blob = await sp.web.lists.getByTitle("Documents").rootFolder.files.getByName("profile.jpg").getBlob();
+
+await sp.profiles.setMyProfilePic(blob);
+
+

Sets single value User Profile property

+

accountName The account name of the user +propertyName Property name +propertyValue Property value

+
setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise<void>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+await sp.profiles.setSingleValueProfileProperty(loginName, "CellPhone", "(123) 555-1212");
+
+

Sets a mult-value User Profile property

+

accountName The account name of the user +propertyName Property name +propertyValues Property values

+
setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>
+
+
const sp = spfi(...);
+const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
+const propertyName = "SPS-Skills";
+const propertyValues = ["SharePoint", "Office 365", "Architecture", "Azure"];
+await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues);
+const profile = await sp.profiles.getPropertiesFor(loginName);
+var props = {};
+profile.UserProfileProperties.forEach((prop) => {
+  props[prop.Key] = prop.Value;
+});
+profile.userProperties = props;
+console.log(profile.userProperties[propertyName]);
+
+

Create Personal Site for specified users

+

Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) +Emails The email addresses of the users to provision sites for

+
createPersonalSiteEnqueueBulk(...emails: string[]): Promise<void>
+
+
const sp = spfi(...);
+let userEmails: string[] = ["testuser1@mytenant.onmicrosoft.com", "testuser2@mytenant.onmicrosoft.com"];
+await sp.profiles.createPersonalSiteEnqueueBulk(userEmails);
+
+

Get the user profile of the owner for the current site

+
ownerUserProfile(): Promise<IUserProfile>
+
+
const sp = spfi(...);
+const profile = await sp.profiles.ownerUserProfile();
+
+

Get the user profile of the current user

+
userProfile(): Promise<any>
+
+
const sp = spfi(...);
+const profile = await sp.profiles.userProfile();
+
+

Create personal site for current user

+
createPersonalSite(interactiveRequest = false): Promise<void>
+
+
const sp = spfi(...);
+await sp.profiles.createPersonalSite();
+
+

Make all profile data public or private

+

Set the privacy settings for all social data.

+
shareAllSocialData(share: boolean): Promise<void>
+
+
const sp = spfi(...);
+await sp.profiles.shareAllSocialData(true);
+
+

Resolve a user or group

+

Resolves user or group using specified query parameters

+
clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity>
+
+
const sp = spfi(...);
+const result = await sp.profiles.clientPeoplePickerResolveUser({
+  AllowEmailAddresses: true,
+  AllowMultipleEntities: false,
+  MaximumEntitySuggestions: 25,
+  QueryString: 'mbowen@contoso.com'
+});
+
+

Search a user or group

+

Searches for users or groups using specified query parameters

+
clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity[]>
+
+
const sp = spfi(...);
+const result = await sp.profiles.clientPeoplePickerSearchUser({
+  AllowEmailAddresses: true,
+  AllowMultipleEntities: false,
+  MaximumEntitySuggestions: 25,
+  QueryString: 'John'
+});
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/publishing-sitepageservice/index.html b/sp/publishing-sitepageservice/index.html new file mode 100644 index 000000000..a1fa3b736 --- /dev/null +++ b/sp/publishing-sitepageservice/index.html @@ -0,0 +1,2672 @@ + + + + + + + + + + + + + + + + + + + + + + + + SP.Publishing.SitePageService - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/publishing-sitepageservice

+

Selective Imports Banner

+

Through the REST api you are able to call a SP.Publishing.SitePageService method GetCurrentUserMemberships. This method allows you to fetch identifiers of unified groups to which current user belongs. It's an alternative for using graph.me.transitiveMemberOf() method from graph package. Note, method only works with the context of a logged in user, and not with app-only permissions.

+

Get current user's group memberships

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/publishing-sitepageservice";
+
+const sp = spfi(...);
+
+const groupIdentifiers = await sp.publishingSitePageService.getCurrentUserMemberships();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/recycle-bin/index.html b/sp/recycle-bin/index.html new file mode 100644 index 000000000..ea7ea8cde --- /dev/null +++ b/sp/recycle-bin/index.html @@ -0,0 +1,2764 @@ + + + + + + + + + + + + + + + + + + + + + + + + Recycle Bin - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

@pnp/sp/recycle-bin

+

The contents of the recycle bin.

+

IRecycleBin, IRecycleBinItem

+

Invokable Banner Selective Imports Banner

+

Work with the contents of the web's Recycle Bin

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/recycle-bin";
+
+const sp = spfi(...);
+
+// gets contents of the web's recycle bin
+const bin = await sp.web.recycleBin();
+
+// gets a specific item from the recycle bin
+const rbItem = await sp.web.recycleBin.getById(bin[0].id);
+
+// delete the item from the recycle bin
+await rbItem.delete();
+
+// restore the item from the recycle bin
+await rbItem.restore();
+
+// move the item to the second-stage (site) recycle bin.
+await rbItem.moveToSecondStage();
+
+// deletes everything in the recycle bin
+await sp.web.recycleBin.deleteAll();
+
+// restores everything in the recycle bin
+await sp.web.recycleBin.restoreAll();
+
+// moves contents of recycle bin to second-stage (site) recycle bin.
+await sp.web.recycleBin.moveAllToSecondStage();
+
+// deletes contents of the second-stage (site) recycle bin.
+await sp.web.recycleBin.deleteAllSecondStageItems();
+
+

Work with the contents of the Second-stage (site) Recycle Bin

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import "@pnp/sp/recycle-bin";
+
+const sp = spfi(...);
+
+// gets contents of the second-stage recycle bin
+const ssBin = await sp.site.recycleBin();
+
+// gets a specific item from the second-stage recycle bin
+const rbItem = await sp.site.recycleBin.getById(ssBin[0].id);
+
+// delete the item from the second-stage recycle bin
+await rbItem.delete();
+
+// restore the item from the second-stage recycle bin
+await rbItem.restore();
+
+// deletes everything in the second-stage recycle bin
+await sp.site.recycleBin.deleteAll();
+
+// restores everything in the second-stage recycle bin
+await sp.site.recycleBin.restoreAll();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/regional-settings/index.html b/sp/regional-settings/index.html new file mode 100644 index 000000000..75fa0b22f --- /dev/null +++ b/sp/regional-settings/index.html @@ -0,0 +1,2808 @@ + + + + + + + + + + + + + + + + + + + + + + + + Regional Settings - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/regional-settings

+

The regional settings module helps with managing dates and times across various timezones.

+

IRegionalSettings

+

Invokable Banner Selective Imports Banner

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings/web";
+
+const sp = spfi(...);
+
+// get all the web's regional settings
+const s = await sp.web.regionalSettings();
+
+// select only some settings to return
+const s2 = await sp.web.regionalSettings.select("DecimalSeparator", "ListSeparator", "IsUIRightToLeft")();
+
+

Installed Languages

+

You can get a list of the installed languages in the web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings/web";
+
+const sp = spfi(...);
+
+const s = await sp.web.regionalSettings.getInstalledLanguages();
+
+
+

The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.

+
+

TimeZones

+

You can also get information about the selected timezone in the web and all of the defined timezones.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings/web";
+
+const sp = spfi(...);
+
+// get the web's configured timezone
+const s = await sp.web.regionalSettings.timeZone();
+
+// select just the Description and Id
+const s2 = await sp.web.regionalSettings.timeZone.select("Description", "Id")();
+
+// get all the timezones
+const s3 = await sp.web.regionalSettings.timeZones();
+
+// get a specific timezone by id
+// list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx
+const s4 = await sp.web.regionalSettings.timeZones.getById(23);
+const s5 = await s.localTimeToUTC(new Date());
+
+// convert a given date from web's local time to UTC time
+const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date());
+
+// convert a given date from UTC time to web's local time
+const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date())
+const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))
+
+

Title and Description Resources

+

Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/regional-settings";
+
+const sp = spfi(...);
+
+//
+// The below methods appears on
+// - Web
+// - List
+// - Field
+// - ContentType
+// - User Custom Action
+//
+// after you import @pnp/sp/regional-settings
+//
+// you can also import just parts of the regional settings:
+// import "@pnp/sp/regional-settings/web";
+// import "@pnp/sp/regional-settings/list";
+// import "@pnp/sp/regional-settings/content-type";
+// import "@pnp/sp/regional-settings/field";
+// import "@pnp/sp/regional-settings/user-custom-actions";
+
+
+const title = await sp.web.titleResource("en-us");
+const title2 = await sp.web.titleResource("de-de");
+
+const description = await sp.web.descriptionResource("en-us");
+const description2 = await sp.web.descriptionResource("de-de");
+
+
+

You can only read the values through the REST API, not set the value.

+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/related-items/index.html b/sp/related-items/index.html new file mode 100644 index 000000000..92e3cc8cc --- /dev/null +++ b/sp/related-items/index.html @@ -0,0 +1,2852 @@ + + + + + + + + + + + + + + + + + + + + + + + + Related Items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/related-items

+

The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.

+

Setup

+

Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work.

+
import { spfi, SPFx, extractWebUrl } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/related-items/web";
+import "@pnp/sp/lists/web";
+import "@pnp/sp/items/list";
+import "@pnp/sp/files/list";
+import { IList } from "@pnp/sp/lists";
+import { getRandomString } from "@pnp/core";
+
+const sp = spfi(...);
+
+// setup some lists (or just use existing ones this is just to show the complete process)
+// we need two lists to use for creating related items, they need to use template 107 (task list)
+const ler1 = await sp.web.lists.ensure("RelatedItemsSourceList", "", 107);
+const ler2 = await sp.web.lists.ensure("RelatedItemsTargetList", "", 107);
+
+const sourceList = ler1.list;
+const targetList = ler2.list;
+
+const sourceListName = await sourceList.select("Id")().then(r => r.Id);
+const targetListName = await targetList.select("Id")().then(r => r.Id);
+
+// or whatever you need to get the web url, both our example lists are in the same web.
+const webUrl = sp.web.toUrl();
+
+// ...individual samples start here
+
+ +
const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+

addSingleLinkToUrl

+

This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document.

+
// get a file's server relative url in some manner, here we add one
+const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, "Content", true).then(r => r.data);
+// add an item or get an item from the task list
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);
+
+

addSingleLinkFromUrl

+

This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.

+ +

This method allows you to delete a link previously created.

+
const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add the link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+// delete the link
+await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+

getRelatedItems

+

Gets the related items for an item

+
import { IRelatedItem } from "@pnp/sp/related-items";
+
+const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);
+
+const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id);
+
+// items.length === 2
+
+

Related items are defined by the IRelatedItem interface

+
export interface IRelatedItem {
+    ListId: string;
+    ItemId: number;
+    Url: string;
+    Title: string;
+    WebId: string;
+    IconUrl: string;
+}
+
+

getPageOneRelatedItems

+

Gets an abbreviated set of related items

+
import { IRelatedItem } from "@pnp/sp/related-items";
+
+const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
+
+const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
+
+// add a link
+await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);
+
+const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id);
+
+// items.length === 2
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/search/index.html b/sp/search/index.html new file mode 100644 index 000000000..0f31daa9c --- /dev/null +++ b/sp/search/index.html @@ -0,0 +1,2913 @@ + + + + + + + + + + + + + + + + + + + + + + + + Search - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/search

+

Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.

+ +

Invokable Banner Selective Imports Banner

+

Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { ISearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+// text search using SharePoint default values for other parameters
+const results: SearchResults = await sp.search("test");
+
+console.log(results.ElapsedTime);
+console.log(results.RowCount);
+console.log(results.PrimarySearchResults);
+
+
+// define a search query object matching the ISearchQuery interface
+const results2: SearchResults = await sp.search(<ISearchQuery>{
+    Querytext: "test",
+    RowLimit: 10,
+    EnableInterleaving: true,
+});
+
+console.log(results2.ElapsedTime);
+console.log(results2.RowCount);
+console.log(results2.PrimarySearchResults);
+
+// define a query using a builder
+const builder = SearchQueryBuilder("test").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites;
+const results3 = await sp.search(builder);
+
+console.log(results3.ElapsedTime);
+console.log(results3.RowCount);
+console.log(results3.PrimarySearchResults);
+
+

Search Result Caching

+

Starting with v3 you can use any of the caching behaviors with search and the results will be cached. Please see here for more details on caching options.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { ISearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
+import { Caching } from "@pnp/queryable";
+
+const sp = spfi(...).using(Caching());
+
+sp.search({/* ... query */}).then((r: SearchResults) => {
+
+    console.log(r.ElapsedTime);
+    console.log(r.RowCount);
+    console.log(r.PrimarySearchResults);
+});
+
+// use a query builder
+const builder = SearchQueryBuilder("test").rowLimit(3);
+
+// supply a search query builder and caching options
+const results2 = await sp.search(builder);
+
+console.log(results2.TotalRows);
+
+

Paging with SearchResults.getPage

+

Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+// this will hold our current results
+let currentResults: SearchResults = null;
+let page = 1;
+
+// triggered on page load or through some other means
+function onStart() {
+
+    // construct our query that will be used throughout the paging process, likely from user input
+    const q = SearchQueryBuilder("test").rowLimit(5);
+    const results = await sp.search(q);
+    currentResults = results; // set the current results
+    page = 1; // reset page counter
+    // update UI...
+}
+
+// triggered by an event
+async function next() {
+
+    currentResults = await currentResults.getPage(++page);
+    // update UI...
+}
+
+// triggered by an event
+async function prev() {
+
+    currentResults = await currentResults.getPage(--page);
+    // update UI...
+}
+
+

SearchQueryBuilder

+

The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { SearchQueryBuilder, SearchResults, ISearchQuery } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+// basic usage
+let q = SearchQueryBuilder().text("test").rowLimit(4).enablePhonetic;
+
+sp.search(q).then(h => { /* ... */ });
+
+// provide a default query text at creation
+let q2 = SearchQueryBuilder("text").rowLimit(4).enablePhonetic;
+
+const results: SearchResults = await sp.search(q2);
+
+// provide query text and a template for
+// shared settings across queries that can
+// be overwritten by individual builders
+const appSearchSettings: ISearchQuery = {
+    EnablePhonetic: true,
+    HiddenConstraints: "reports"
+};
+
+let q3 = SearchQueryBuilder("test", appSearchSettings).enableQueryRules;
+let q4 = SearchQueryBuilder("financial data", appSearchSettings).enableSorting.enableStemming;
+const results2 = await sp.search(q3);
+const results3 = sp.search(q4);
+
+

Search Suggest

+

Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/search";
+import { ISuggestQuery, ISuggestResult } from "@pnp/sp/search";
+
+const sp = spfi(...);
+
+const results = await sp.searchSuggest("test");
+
+const results2 = await sp.searchSuggest({
+    querytext: "test",
+    count: 5,
+} as ISuggestQuery);
+
+

Search Factory

+

You can also configure a search or suggest query against any valid SP url using the factory methods.

+
+

In this case you'll need to ensure you add observers, or use the tuple constructor to inherit

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/web";
+import "@pnp/sp/search";
+import { Search, Suggest } from "@pnp/sp/search";
+import { SPDefault } from "@pnp/nodejs";
+
+const sp = spfi(...);
+
+// set the url for search
+const searcher = Search([sp.web, "https://mytenant.sharepoint.com/sites/dev"]);
+
+// this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder)
+const results = await searcher("test");
+
+// you can reuse the ISearch instance
+const results2 = await searcher("another query");
+
+// same process works for Suggest
+const suggester = Suggest([sp.web, "https://mytenant.sharepoint.com/sites/dev"]);
+
+const suggestions = await suggester({ querytext: "test" });
+
+// resetting the observers on the instance
+const searcher2 = Search("https://mytenant.sharepoint.com/sites/dev").using(SPDefault({
+  msal: {
+    config: {...},
+    scopes: [...],
+  },
+}));
+
+const results3 = await searcher2("test");
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/security/index.html b/sp/security/index.html new file mode 100644 index 000000000..276cb1949 --- /dev/null +++ b/sp/security/index.html @@ -0,0 +1,2903 @@ + + + + + + + + + + + + + + + + + + + + + + + + Security - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/security

+

There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below.

+
+

Site permissions are managed on the root web of the site collection.

+
+

A Note on Selective Imports for Security

+

Because the method are shared you can opt to import only the methods for one of the instances.

+
import "@pnp/sp/security/web";
+import "@pnp/sp/security/list";
+import "@pnp/sp/security/item";
+
+

Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module:

+
import "@pnp/sp/security";
+
+

Securable Methods

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/security/list";
+import "@pnp/sp/site-users/web";
+import { IList } from "@pnp/sp/lists";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+// ensure we have a list
+const ler = await sp.web.lists.ensure("SecurityTestingList");
+const list: IList = ler.list;
+
+// role assignments (see section below)
+await list.roleAssignments();
+
+// data will represent one of the possible parents Site, Web, or List
+const data = await list.firstUniqueAncestorSecurableObject();
+
+// getUserEffectivePermissions
+const users = await sp.web.siteUsers.top(1).select("LoginName")();
+const perms = await list.getUserEffectivePermissions(users[0].LoginName);
+
+// getCurrentUserEffectivePermissions
+const perms2 = list.getCurrentUserEffectivePermissions();
+
+// userHasPermissions
+const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems)
+
+// currentUserHasPermissions
+const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems)
+
+// breakRoleInheritance
+await list.breakRoleInheritance();
+// copy existing permissions
+await list.breakRoleInheritance(true);
+// copy existing permissions and reset all child securables to the new permissions
+await list.breakRoleInheritance(true, true);
+
+// resetRoleInheritance
+await list.resetRoleInheritance();
+
+

Web Specific methods

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/security/web";
+
+const sp = spfi(...);
+
+// role definitions (see section below)
+const defs = await sp.web.roleDefinitions();
+
+

Role Assignments

+

Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/security/web";
+import "@pnp/sp/site-users/web";
+import { IList } from "@pnp/sp/lists";
+import { PermissionKind } from "@pnp/sp/security";
+
+const sp = spfi(...);
+
+// ensure we have a list
+const ler = await sp.web.lists.ensure("SecurityTestingList");
+const list: IList = ler.list;
+
+// list role assignments
+const assignments = await list.roleAssignments();
+
+// add a role assignment
+const defs = await sp.web.roleDefinitions();
+const user = await sp.web.currentUser();
+const r = await list.roleAssignments.add(user.Id, defs[0].Id);
+
+// remove a role assignment
+const { Id: fullRoleDefId } = await sp.web.roleDefinitions.getByName('Full Control')();
+const ras = await list.roleAssignments();
+// filter/find the role assignment you want to remove
+// here we just grab the first
+const ra = ras.find(v => true);
+const r = await list.roleAssignments.remove(ra.PrincipalId, fullRoleDefId);
+
+// read role assignment info
+const info = await list.roleAssignments.getById(ra.Id)();
+
+// get the groups
+const info2 = await list.roleAssignments.getById(ra.Id).groups();
+
+// get the bindings
+const info3 = await list.roleAssignments.getById(ra.Id).bindings();
+
+// delete a role assignment (same as remove)
+const ras = await list.roleAssignments();
+// filter/find the role assignment you want to remove
+// here we just grab the first
+const ra = ras.find(v => true);
+
+// delete it
+await list.roleAssignments.getById(ra.Id).delete();
+
+

Role Definitions

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/security/web";
+
+const sp = spfi(...);
+
+// read role definitions
+const defs = await sp.web.roleDefinitions();
+
+// get by id
+const def = await sp.web.roleDefinitions.getById(5)();
+const def = await sp.web.roleDefinitions.getById(5).select("Name", "Order")();
+
+// get by name
+const def = await sp.web.roleDefinitions.getByName("Full Control")();
+const def = await sp.web.roleDefinitions.getByName("Full Control").select("Name", "Order")();
+
+// get by type
+const def = await sp.web.roleDefinitions.getByType(5)();
+const def = await sp.web.roleDefinitions.getByType(5).select("Name", "Order")();
+
+// add
+// name The new role definition's name
+// description The new role definition's description
+// order The order in which the role definition appears
+// basePermissions The permissions mask for this role definition
+const rdar = await sp.web.roleDefinitions.add("title", "description", 99, { High: 1, Low: 2 });
+
+
+
+// the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example
+
+// delete
+await sp.web.roleDefinitions.getById(5).delete();
+
+// update
+const res = sp.web.roleDefinitions.getById(5).update({ Name: "New Name" });
+
+

Get List Items with Unique Permissions

+

In order to get a list of items that have unique permissions you have to specifically select the '' field and then filter on the client.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+import "@pnp/sp/security/items";
+
+const sp = spfi(...);
+
+const listItems = await sp.web.lists.getByTitle("pnplist").items.select("Id, HasUniqueRoleAssignments")();
+
+//Loop over list items filtering for HasUniqueRoleAssignments value
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/sharing/index.html b/sp/sharing/index.html new file mode 100644 index 000000000..af5a54e6d --- /dev/null +++ b/sp/sharing/index.html @@ -0,0 +1,3056 @@ + + + + + + + + + + + + + + + + + + + + + + + + Sharing - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+ +
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/sharing

+
+

Note: This API is still considered "beta" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.

+
+

One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.

+

Imports

+

In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.

+

Import All

+

To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module:

+
import "@pnp/sp/sharing";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+const user = await sp.web.siteUsers.getByEmail("user@site.com")();
+const r = await sp.web.shareWith(user.LoginName);
+
+

Selective Import

+

Import only the web's sharing methods into the library

+
import "@pnp/sp/sharing/web";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { spfi } from "@pnp/sp";
+
+const sp = spfi(...);
+
+const user = await sp.web.siteUsers.getByEmail("user@site.com")();
+const r = await sp.web.shareWith(user.LoginName);
+
+ +

Applies to: Item, Folder, File

+

Creates a sharing link for the given resource with an optional expiration.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import { SharingLinkKind, IShareLinkResponse } from "@pnp/sp/sharing";
+import { dateAdd } from "@pnp/core";
+
+const sp = spfi(...);
+
+const result = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView);
+
+console.log(JSON.stringify(result, null, 2));
+
+
+const result2 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), "day", 5));
+
+console.log(JSON.stringify(result2, null, 2));
+
+

shareWith

+

Applies to: Item, Folder, File, Web

+

Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter "shareEverything" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.

+

Batching Not Supported Banner

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders/web";
+import "@pnp/sp/files/web";
+import { ISharingResult, SharingRole } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result = await sp.web.shareWith("i:0#.f|membership|user@site.com");
+
+console.log(JSON.stringify(result, null, 2));
+
+// Share and allow editing
+const result2 = await sp.web.shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit);
+
+console.log(JSON.stringify(result2, null, 2));
+
+
+// share folder
+const result3 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").shareWith("i:0#.f|membership|user@site.com");
+
+// Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children)
+await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit, true, true);
+
+// Share a file
+await sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com");
+
+// Share a file with edit permissions
+await sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit);
+
+

shareObject & shareObjectRaw

+

Applies to: Web

+

Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import { ISharingResult, SharingRole } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// Share an object in this web
+const result = await sp.web.shareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt", "i:0#.f|membership|user@site.com", SharingRole.View);
+
+// Share an object with all settings available
+await sp.web.shareObjectRaw({
+    url: "https://mysite.sharepoint.com/sites/dev/Docs/test.txt",
+    peoplePickerInput: [{ Key: "i:0#.f|membership|user@site.com" }],
+    roleValue: "role: 1973741327",
+    groupId: 0,
+    propagateAcl: false,
+    sendEmail: true,
+    includeAnonymousLinkInEmail: false,
+    emailSubject: "subject",
+    emailBody: "body",
+    useSimplifiedRoles: true,
+});
+
+

unshareObject

+

Applies to: Web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import { ISharingResult } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result = await sp.web.unshareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt");
+
+

checkSharingPermissions

+

Applies to: Item, Folder, File

+

Checks Permissions on the list of Users and returns back role the users have on the Item.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing/folders";
+import "@pnp/sp/folders/web";
+import { SharingEntityPermission } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// check the sharing permissions for a folder
+const perms = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").checkSharingPermissions([{ alias: "i:0#.f|membership|user@site.com" }]);
+
+

getSharingInformation

+

Applies to: Item, Folder, File

+

Get Sharing Information.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { ISharingInformation } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// Get the sharing information for a folder
+const info = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation();
+
+// get sharing informaiton with a request object
+const info2 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation({
+    maxPrincipalsToReturn: 10,
+    populateInheritedLinks: true,
+});
+
+// get sharing informaiton using select and expand, NOTE expand comes first in the API signature
+const info3 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation({}, ["permissionsInformation"], ["permissionsInformation","anyoneLinkTrackUsers"]);
+
+

getObjectSharingSettings

+

Applies to: Item, Folder, File

+

Gets the sharing settings

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { IObjectSharingSettings } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+// Gets the sharing object settings
+const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getObjectSharingSettings();
+
+

unshare

+

Applies to: Item, Folder, File

+

Unshares a given resource

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { ISharingResult } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshare();
+
+

deleteSharingLinkByKind

+

Applies to: Item, Folder, File

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { ISharingResult, SharingLinkKind } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);
+
+ +

Applies to: Item, Folder, File

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/sharing";
+import "@pnp/sp/folders";
+import { SharingLinkKind } from "@pnp/sp/sharing";
+
+const sp = spfi(...);
+
+await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit);
+
+// specify the sharing link id if available
+await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit, "12345");
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/site-designs/index.html b/sp/site-designs/index.html new file mode 100644 index 000000000..663c0704c --- /dev/null +++ b/sp/site-designs/index.html @@ -0,0 +1,2841 @@ + + + + + + + + + + + + + + + + + + + + + + + + Site Designs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/site-designs

+

You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. +Check out SharePoint site design and site script overview for more information.

+

Site Designs

+

Selective Imports Banner

+

Create a new site design

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// WebTemplate: 64 Team site template, 68 Communication site template
+const siteDesign = await sp.siteDesigns.createSiteDesign({
+    SiteScriptIds: ["884ed56b-1aab-4653-95cf-4be0bfa5ef0a"],
+    Title: "SiteDesign001",
+    WebTemplate: "64",
+});
+
+console.log(siteDesign.Title);
+
+

Applying a site design to a site

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Limited to 30 actions in a site script, but runs synchronously
+await sp.siteDesigns.applySiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8","https://contoso.sharepoint.com/sites/teamsite-pnpjs001");
+
+// Better use the following method for 300 actions in a site script
+const task = await sp.web.addSiteDesignTask("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+

Retrieval

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Retrieving all site designs
+const allSiteDesigns = await sp.siteDesigns.getSiteDesigns();
+console.log(`Total site designs: ${allSiteDesigns.length}`);
+
+// Retrieving a single site design by Id
+const siteDesign = await sp.siteDesigns.getSiteDesignMetadata("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+console.log(siteDesign.Title);
+
+

Update and delete

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Update
+const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: "75b9d8fe-4381-45d9-88c6-b03f483ae6a8", Title: "SiteDesignUpdatedTitle001" });
+
+// Delete
+await sp.siteDesigns.deleteSiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+

Setting Rights/Permissions

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+// Get
+const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+console.log(rights.length > 0 ? rights[0].PrincipalName : "");
+
+// Grant
+await sp.siteDesigns.grantSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
+
+// Revoke
+await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
+
+// Reset all view rights
+const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", rights.map(u => u.PrincipalName));
+
+

Get a history of site designs that have run on a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-designs";
+
+const sp = spfi(...);
+
+const runs = await sp.web.getSiteDesignRuns();
+const runs2 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite");
+
+// Get runs specific to a site design
+const runs3 = await sp.web.getSiteDesignRuns("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+const runs4 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite", "75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+// For more information about the site script actions
+const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID);
+const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus("https://TENANT.sharepoint.com/sites/mysite", runs[0].ID);
+
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/site-groups/index.html b/sp/site-groups/index.html new file mode 100644 index 000000000..b846a752f --- /dev/null +++ b/sp/site-groups/index.html @@ -0,0 +1,2929 @@ + + + + + + + + + + + + + + + + + + + + + + + + Site Groups - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/site-groups

+

The site groups module provides methods to manage groups for a sharepoint site.

+

ISiteGroups

+

Invokable Banner Selective Imports Banner

+

Get all site groups

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// gets all site groups of the web
+const groups = await sp.web.siteGroups();
+
+

Get the associated groups of a web

+

You can get the associated Owner, Member and Visitor groups of a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// Gets the associated visitors group of a web
+const visitorGroup = await sp.web.associatedVisitorGroup();
+
+// Gets the associated members group of a web
+const memberGroup = await sp.web.associatedMemberGroup();
+
+// Gets the associated owners group of a web
+const ownerGroup = await sp.web.associatedOwnerGroup();
+
+
+

Create the default associated groups for a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// Breaks permission inheritance and creates the default associated groups for the web
+
+// Login name of the owner
+const owner1 = "owner@example.onmicrosoft.com";
+
+// Specify true, the permissions should be copied from the current parent scope, else false
+const copyRoleAssignments = false;
+
+// Specify true to make all child securable objects inherit role assignments from the current object
+const clearSubScopes = true;
+
+await sp.web.createDefaultAssociatedGroups("PnP Site", owner1, copyRoleAssignments, clearSubScopes);
+
+

Create a new site group

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+// Creates a new site group with the specified title
+await sp.web.siteGroups.add({"Title":"new group name"});
+
+

ISiteGroup

+

Invokable Banner Selective Imports Banner

+ + + + + + + + + + + + + + + + + + + + + +
ScenarioImport Statement
Selective 2import "@pnp/sp/webs";
import "@pnp/sp/site-groups";
Selective 3import "@pnp/sp/webs";
import "@pnp/sp/site-groups/web";
Preset: Allimport {sp, SiteGroups, SiteGroup } from "@pnp/sp/presets/all";
+

Getting and updating the groups of a sharepoint web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups";
+
+const sp = spfi(...);
+
+// get the group using a group id
+const groupID = 33;
+let grp = await sp.web.siteGroups.getById(groupID)();
+
+// get the group using the group's name
+const groupName = "ClassicTeam Visitors";
+grp = await sp.web.siteGroups.getByName(groupName)();
+
+// update a group
+await sp.web.siteGroups.getById(groupID).update({"Title": "New Group Title"});
+
+// delete a group from the site using group id
+await sp.web.siteGroups.removeById(groupID);
+
+// delete a group from the site using group name
+await sp.web.siteGroups.removeByLoginName(groupName);
+
+

Getting all users of a group

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups";
+
+const sp = spfi(...);
+
+// get all users of group
+const groupID = 7;
+const users = await sp.web.siteGroups.getById(groupID).users();
+
+

Updating the owner of a site group

+

Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups";
+
+const sp = spfi(...);
+
+// Update the owner with a user id
+await sp.web.siteGroups.getById(7).setUserAsOwner(4);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/site-scripts/index.html b/sp/site-scripts/index.html new file mode 100644 index 000000000..b6d065af8 --- /dev/null +++ b/sp/site-scripts/index.html @@ -0,0 +1,2858 @@ + + + + + + + + + + + + + + + + + + + + + + + + Site Scripts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

@pnp/sp/site-scripts

+

Selective Imports Banner

+

Create a new site script

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+const sitescriptContent = {
+    "$schema": "schema.json",
+    "actions": [
+        {
+            "themeName": "Theme Name 123",
+            "verb": "applyTheme",
+        },
+    ],
+    "bindata": {},
+    "version": 1,
+};
+
+const siteScript = await sp.siteScripts.createSiteScript("Title", "description", sitescriptContent);
+
+console.log(siteScript.Title);
+
+

Retrieval

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+// Retrieving all site scripts
+const allSiteScripts = await sp.siteScripts.getSiteScripts();
+console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : "");
+
+// Retrieving a single site script by Id
+const siteScript = await sp.siteScripts.getSiteScriptMetadata("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
+console.log(siteScript.Title);
+
+

Update and delete

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+// Update
+const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: "884ed56b-1aab-4653-95cf-4be0bfa5ef0a", Title: "New Title" });
+console.log(updatedSiteScript.Title);
+
+// Delete
+await sp.siteScripts.deleteSiteScript("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
+
+

Get site script from a list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+// Using the absolute URL of the list
+const ss = await sp.siteScripts.getSiteScriptFromList("https://TENANT.sharepoint.com/Lists/mylist");
+
+// Using the PnPjs web object to fetch the site script from a specific list
+const ss2 = await sp.web.lists.getByTitle("mylist").getSiteScript();
+
+

Get site script from a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const extractInfo = {
+    IncludeBranding: true,
+    IncludeLinksToExportedItems: true,
+    IncludeRegionalSettings: true,
+    IncludeSiteExternalSharingCapability: true,
+    IncludeTheme: true,
+    IncludedLists: ["Lists/MyList"]
+};
+
+const ss = await sp.siteScripts.getSiteScriptFromWeb("https://TENANT.sharepoint.com/sites/mysite", extractInfo);
+
+// Using the PnPjs web object to fetch the site script from a specific web
+const ss2 = await sp.web.getSiteScript(extractInfo);
+
+

Execute Site Script Action

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/site-scripts";
+
+const sp = spfi(...);
+
+const siteScript = "your site script action...";
+
+const ss = await sp.siteScripts.executeSiteScriptAction(siteScript);
+
+

Execute site script for a specific web

+
import { spfi } from "@pnp/sp";
+import { SiteScripts } "@pnp/sp/site-scripts";
+
+const siteScript = "your site script action...";
+
+const scriptService = SiteScripts("https://absolute/url/to/web");
+
+const ss = await scriptService.executeSiteScriptAction(siteScript);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/site-users/index.html b/sp/site-users/index.html new file mode 100644 index 000000000..33f88a758 --- /dev/null +++ b/sp/site-users/index.html @@ -0,0 +1,3070 @@ + + + + + + + + + + + + + + + + + + + + + + + + Site Users - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/site-users

+

The site users module provides methods to manage users for a sharepoint site.

+

ISiteUsers

+

Invokable Banner Selective Imports Banner

+

Get all site user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const users = await sp.web.siteUsers();
+
+

Get Current user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+let user = await sp.web.currentUser();
+
+

Get user by id

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const id = 6;
+user = await sp.web.getUserById(id)();
+
+

Ensure user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const username = "usernames@microsoft.com";
+result = await sp.web.ensureUser(username);
+
+

ISiteUser

+

Invokable Banner Selective Imports Banner

+ + + + + + + + + + + + + + + + + + + + + +
ScenarioImport Statement
Selective 2import "@pnp/sp/webs";
import "@pnp/sp/site-users";
Selective 3import "@pnp/sp/webs";
import "@pnp/sp/site-users/web";
Preset: Allimport {sp, SiteUsers, SiteUser } from "@pnp/sp/presets/all";
+

Get user Groups

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+let groups = await sp.web.currentUser.groups();
+
+

Add user to Site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const user = await sp.web.ensureUser("userLoginname")
+const users = await sp.web.siteUsers;
+
+await users.add(user.data.LoginName);
+
+

Get user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+// get user object by id
+const user = await sp.web.siteUsers.getById(6)();
+
+//get user object by Email
+const user = await sp.web.siteUsers.getByEmail("user@mail.com")();
+
+//get user object by LoginName
+const user = await sp.web.siteUsers.getByLoginName("userLoginName")();
+
+

Update user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+let userProps = await sp.web.currentUser();
+userProps.Title = "New title";
+await sp.web.currentUser.update(userProps);
+
+

Remove user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+// remove user by id
+await sp.web.siteUsers.removeById(6);
+
+// remove user by LoginName
+await sp.web.siteUsers.removeByLoginName(6);
+
+

ISiteUserProps

+

User properties:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Property NameTypeDescription
EmailstringContains Site user email
IdNumberContains Site user Id
IsHiddenInUIBooleanSite user IsHiddenInUI
IsShareByEmailGuestUserbooleanSite user is external user
IsSiteAdminBooleanDescribes if Site user Is Site Admin
LoginNamestringSite user LoginName
PrincipalTypenumberSite user Principal type
TitlestringSite user Title
+
interface ISiteUserProps {
+
+    /**
+     * Contains Site user email
+     *
+     */
+    Email: string;
+
+    /**
+     * Contains Site user Id
+     *
+     */
+    Id: number;
+
+    /**
+     * Site user IsHiddenInUI
+     *
+     */
+    IsHiddenInUI: boolean;
+
+    /**
+     * Site user IsShareByEmailGuestUser
+     *
+     */
+    IsShareByEmailGuestUser: boolean;
+
+    /**
+     * Describes if Site user Is Site Admin
+     *
+     */
+    IsSiteAdmin: boolean;
+
+    /**
+     * Site user LoginName
+     *
+     */
+    LoginName: string;
+
+    /**
+     * Site user Principal type
+     *
+     */
+    PrincipalType: number | PrincipalType;
+
+    /**
+     * Site user Title
+     *
+     */
+    Title: string;
+}
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/sites/index.html b/sp/sites/index.html new file mode 100644 index 000000000..fd7083433 --- /dev/null +++ b/sp/sites/index.html @@ -0,0 +1,3210 @@ + + + + + + + + + + + + + + + + + + + + + + + + Sites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/site - Site properties

+

Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.

+

Get context information for the current site collection

+

Using the library, you can get the context information of the current site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import { IContextInfo } from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const oContext: IContextInfo = await sp.site.getContextInfo();
+console.log(oContext.FormDigestValue);
+
+

Get document libraries of a web

+

Using the library, you can get a list of the document libraries present in the a given web.

+

Note: Works only in SharePoint online

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import { IDocumentLibraryInformation } from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries("https://tenant.sharepoint.com/sites/test/subsite");
+
+//we got the array of document library information
+docLibs.forEach((docLib: IDocumentLibraryInformation) => {
+    // do something with each library
+});
+
+

Open Web By Id

+

Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const w = await sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e");
+
+//we got all the data from the web as well
+console.log(w.data);
+
+// we can chain
+const w2 = await w.web.select("Title")();
+
+

Get absolute web url from page url

+

Using the library, you can get the absolute web url by providing a page url

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const d: string = await sp.site.getWebUrlFromPageUrl("https://tenant.sharepoint.com/sites/test/Pages/test.aspx");
+
+console.log(d); //https://tenant.sharepoint.com/sites/test
+
+

Access the root web

+

There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. "_api/sites/rootweb" which does not work for all operations.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// use for rootweb information access
+const rootwebData = await sp.site.rootWeb();
+
+// use for chaining
+const rootweb = await sp.site.getRootWeb();
+const listData = await rootWeb.lists.getByTitle("MyList")();
+
+

Create a modern communication site

+

Note: Works only in SharePoint online

+

Creates a modern communication site.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeRequiredDescription
TitlestringyesThe title of the site to create.
lcidnumberyesThe default language to use for the site.
shareByEmailEnabledbooleanyesIf set to true, it will enable sharing files via Email. By default it is set to false
urlstringyesThe fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site.
descriptionstringnoThe description of the communication site.
classificationstringnoThe Site classification to use. For instance "Contoso Classified". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information
siteDesignIdstringnoThe Guid of the site design to be used.
You can use the below default OOTB GUIDs:
Topic: null
Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767
Blank: f6cc5403-0d63-442e-96c0-285923709ffc
hubSiteIdstringnoThe Guid of the already existing Hub site
OwnerstringnoRequired when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com
+

+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const result = await sp.site.createCommunicationSite(
+            "Title",
+            1033,
+            true,
+            "https://tenant.sharepoint.com/sites/commSite",
+            "Description",
+            "HBI",
+            "f6cc5403-0d63-442e-96c0-285923709ffc",
+            "a00ec589-ea9f-4dba-a34e-67e78d41e509",
+            "user@TENANT.onmicrosoft.com");
+
+
+

Create from Props

+

You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// in this case you supply a single struct deinfing the creation props
+const result = await sp.site.createCommunicationSiteFromProps({
+  Owner: "patrick@three18studios.com",
+  Title: "A Test Site",
+  Url: "https://{tenant}.sharepoint.com/sites/commsite2",
+  WebTemplate: "STS#3",
+});
+
+

Create a modern team site

+

Note: Works only in SharePoint online. It wont work with App only tokens

+

Creates a modern team site backed by O365 group.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeRequiredDescription
displayNamestringyesThe title/displayName of the site to be created.
aliasstringyesAlias of the underlying Office 365 Group.
isPublicbooleanyesDefines whether the Office 365 Group will be public (default), or private.
lcidnumberyesThe language to use for the site. If not specified will default to English (1033).
descriptionstringnoThe description of the modern team site.
classificationstringnoThe Site classification to use. For instance "Contoso Classified". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information
ownersstring array (string[])noThe Owners of the site to be created
hubSiteIdstringnoThe Guid of the already existing Hub site
siteDesignIdstringnoThe Guid of the site design to be used.
You can use the below default OOTB GUIDs:
Topic: null
Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767
Blank: f6cc5403-0d63-442e-96c0-285923709ffc
+

+import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+const result = await sp.site.createModernTeamSite(
+        "displayName",
+        "alias",
+        true,
+        1033,
+        "description",
+        "HBI",
+        ["user1@tenant.onmicrosoft.com","user2@tenant.onmicrosoft.com","user3@tenant.onmicrosoft.com"],
+        "a00ec589-ea9f-4dba-a34e-67e78d41e509",
+        "f6cc5403-0d63-442e-96c0-285923709ffc"
+        );
+
+console.log(d);
+
+

Create from Props

+

You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// in this case you supply a single struct deinfing the creation props
+const result = await sp.site.createModernTeamSiteFromProps({
+  alias: "JenniferGarner",
+  displayName: "A Test Site",
+  owners: ["patrick@three18studios.com"],
+});
+
+

Delete a site collection

+

Using the library, you can delete a specific site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import { Site } from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// Delete the current site
+await sp.site.delete();
+
+// Specify which site to delete
+const siteUrl = "https://tenant.sharepoint.com/sites/subsite";
+const site2 = Site(siteUrl);
+await site2.delete();
+
+

Check if a Site Collection Exists

+

Using the library, you can check if a specific site collection exist or not on your tenant

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+// Specify which site to verify
+const siteUrl = "https://tenant.sharepoint.com/sites/subsite";
+const exists = await sp.site.exists(siteUrl);
+console.log(exists);
+
+ +
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sites";
+import {ISiteLogoProperties, SiteLogoAspect, SiteLogoType} from "@pnp/sp/sites";
+
+const sp = spfi(...);
+
+//set the web's site logo
+const logoProperties: ISiteLogoProperties = {
+    relativeLogoUrl: "/sites/mySite/SiteAssets/site_logo.png", 
+    aspect: SiteLogoAspect.Rectangular, 
+    type: SiteLogoType.WebLogo
+};
+await sp.site.setSiteLogo(logoProperties);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/social/index.html b/sp/social/index.html new file mode 100644 index 000000000..8fb3581d7 --- /dev/null +++ b/sp/social/index.html @@ -0,0 +1,2952 @@ + + + + + + + + + + + + + + + + + + + + + + + + Social - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/ - social

+

Selective Imports Banner

+

The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not +with app-only permissions.

+

getFollowedSitesUri

+

Gets a URI to a site that lists the current user's followed sites.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+const uri = await sp.social.getFollowedSitesUri();
+
+

getFollowedDocumentsUri

+

Gets a URI to a site that lists the current user's followed documents.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+const uri = await sp.social.getFollowedDocumentsUri();
+
+

follow

+

Makes the current user start following a user, document, site, or tag

+
import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// follow a site
+const r1 = await sp.social.follow({
+    ActorType: SocialActorType.Site,
+    ContentUri: "htts://tenant.sharepoint.com/sites/site",
+});
+
+// follow a person
+const r2 = await sp.social.follow({
+    AccountName: "i:0#.f|membership|person@tenant.com",
+    ActorType: SocialActorType.User,
+});
+
+// follow a doc
+const r3 = await sp.social.follow({
+    ActorType: SocialActorType.Document,
+    ContentUri: "https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx",
+});
+
+// follow a tag
+// You need the tag GUID to start following a tag.
+// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model.
+// See How to get a tag's GUID based on the tag's name by using the JavaScript object model.
+// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid
+const r4 = await sp.social.follow({
+    ActorType: SocialActorType.Tag,
+    TagGuid: "19a4a484-c1dc-4bc5-8c93-bb96245ce928",
+});
+
+

isFollowed

+

Indicates whether the current user is following a specified user, document, site, or tag

+
import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// pass the same social actor struct as shown in follow example for each type
+const r = await sp.social.isFollowed({
+    AccountName: "i:0#.f|membership|person@tenant.com",
+    ActorType: SocialActorType.User,
+});
+
+

stopFollowing

+

Makes the current user stop following a user, document, site, or tag

+
import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// pass the same social actor struct as shown in follow example for each type
+const r = await sp.social.stopFollowing({
+    AccountName: "i:0#.f|membership|person@tenant.com",
+    ActorType: SocialActorType.User,
+});
+
+

my

+

get

+

Gets this user's social information

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+const r = await sp.social.my();
+
+

followed

+

Gets users, documents, sites, and tags that the current user is following based on the supplied flags.

+
import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get all the followed documents
+const r1 = await sp.social.my.followed(SocialActorTypes.Document);
+
+// get all the followed documents and sites
+const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site);
+
+// get all the followed sites updated in the last 24 hours
+const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);
+
+

followedCount

+

Works as followed but returns on the count of actors specified by the query

+
import { spfi } from "@pnp/sp";
+import { SocialActorType } from "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get the followed documents count
+const r = await sp.social.my.followedCount(SocialActorTypes.Document);
+
+

followers

+

Gets the users who are following the current user.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get the followed documents count
+const r = await sp.social.my.followers();
+
+

suggestions

+

Gets users who the current user might want to follow.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/social";
+
+const sp = spfi(...);
+
+// get the followed documents count
+const r = await sp.social.my.suggestions();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/sp-utilities-utility/index.html b/sp/sp-utilities-utility/index.html new file mode 100644 index 000000000..01a802153 --- /dev/null +++ b/sp/sp-utilities-utility/index.html @@ -0,0 +1,2911 @@ + + + + + + + + + + + + + + + + + + + + + + + + SP.Utilities.Utility - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/utilities

+

Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.

+

sendEmail

+

This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).

+

EmailProperties

+
export interface TypedHash<T> {
+    [key: string]: T;
+}
+
+export interface EmailProperties {
+
+    To: string[];
+    CC?: string[];
+    BCC?: string[];
+    Subject: string;
+    Body: string;
+    AdditionalHeaders?: TypedHash<string>;
+    From?: string;
+}
+
+

Usage

+

You must define the To, Subject, and Body values - the remaining are optional.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+import { IEmailProperties } from "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+const emailProps: IEmailProperties = {
+    To: ["user@site.com"],
+    CC: ["user2@site.com", "user3@site.com"],
+    BCC: ["user4@site.com", "user5@site.com"],
+    Subject: "This email is about...",
+    Body: "Here is the body. <b>It supports html</b>",
+    AdditionalHeaders: {
+        "content-type": "text/html"
+    }
+};
+
+await sp.utility.sendEmail(emailProps);
+console.log("Email Sent!");
+
+

getCurrentUserEmailAddresses

+

This method returns the current user's email addresses known to SharePoint.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let addressString: string = await sp.utility.getCurrentUserEmailAddresses();
+
+// and use it with sendEmail
+await sp.utility.sendEmail({
+    To: [addressString],
+    Subject: "This email is about...",
+    Body: "Here is the body. <b>It supports html</b>",
+    AdditionalHeaders: {
+        "content-type": "text/html"
+    },
+});
+
+

resolvePrincipal

+

Gets information about a principal that matches the specified Search criteria

+
import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let principal : IPrincipalInfo = await sp.utility.resolvePrincipal("user@site.com", PrincipalType.User, PrincipalSource.All, true, false, true);
+
+console.log(principal);
+
+

searchPrincipals

+

Gets information about the principals that match the specified Search criteria.

+
import { spfi, SPFx, IPrincipalInfo, PrincipalType, PrincipalSource } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals("john", PrincipalType.User, PrincipalSource.All,"", 10);
+
+console.log(principals);
+
+

createEmailBodyForInvitation

+

Gets the external (outside the firewall) URL to a document or resource in a site.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let url : string = await sp.utility.createEmailBodyForInvitation("https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx");
+console.log(url);
+
+

expandGroupsToPrincipals

+

Resolves the principals contained within the supplied groups

+
import { spfi, SPFx, IPrincipalInfo } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"]);
+console.log(principals);
+
+// optionally supply a max results count. Default is 30.
+let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"], 10);
+console.log(principals);
+
+

createWikiPage

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/sputilities";
+import { ICreateWikiPageResult } from "@pnp/sp/sputilities";
+
+const sp = spfi(...);
+
+let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({
+    ServerRelativeUrl: "/sites/dev/SitePages/mynewpage.aspx",
+    WikiHtmlContent: "This is my <b>page</b> content. It supports rich html.",
+});
+
+// newPage contains the raw data returned by the service
+console.log(newPage.data);
+
+// newPage contains a File instance you can use to further update the new page
+let file = await newPage.file();
+console.log(file);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/subscriptions/index.html b/sp/subscriptions/index.html new file mode 100644 index 000000000..761b874ea --- /dev/null +++ b/sp/subscriptions/index.html @@ -0,0 +1,2816 @@ + + + + + + + + + + + + + + + + + + + + + + + + Subscriptions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/subscriptions

+

Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.

+

ISubscriptions

+

Invokable Banner Selective Imports Banner

+

Add a webhook

+

Using this library, you can add a webhook to a specified list within the SharePoint site.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+
+import { Subscriptions, ISubscriptions} from "@pnp/sp/subscriptions";
+import "@pnp/sp/subscriptions/list";
+
+const sp = spfi(...);
+
+// This is the URL which will be called by SharePoint when there is a change in the list
+const notificationUrl = "<notification-url>";
+
+// Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date.
+const expiryDate = dateAdd(new Date(), "day" , 180).toISOString();
+
+// Adds a webhook to the Documents library
+var res = await sp.web.lists.getByTitle("Documents").subscriptions.add(notificationUrl,expiryDate);
+
+

Get all webhooks added to a list

+

Read all the webhooks' details which are associated to the list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/subscriptions";
+
+const sp = spfi(...);
+
+const res = await sp.web.lists.getByTitle("Documents").subscriptions();
+
+

ISubscription

+

This interface provides the methods for managing a particular webhook.

+

Invokable Banner Selective Imports Banner

+ + + + + + + + + + + + + + + + + +
ScenarioImport Statement
Selectiveimport "@pnp/sp/webs";
import "@pnp/sp/lists";
import { Subscriptions, ISubscriptions, Subscription, ISubscription} from "@pnp/sp/subscriptions";
import "@pnp/sp/subscriptions/list"
Preset: Allimport { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from "@pnp/sp/presets/all";
+

Managing a webhook

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/subscriptions";
+
+const sp = spfi(...);
+
+// Get details of a webhook based on its ID
+const webhookId = "1f029e5c-16e4-4941-b46f-67895118763f";
+const webhook = await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId)();
+
+// Update a webhook
+const newDate = dateAdd(new Date(), "day" , 150).toISOString();
+const updatedWebhook = await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId).update(newDate);
+
+// Delete a webhook
+await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId).delete();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/taxonomy/index.html b/sp/taxonomy/index.html new file mode 100644 index 000000000..9fc790a23 --- /dev/null +++ b/sp/taxonomy/index.html @@ -0,0 +1,3487 @@ + + + + + + + + + + + + + + + + + + + + + + + + Taxonomy - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/taxonomy

+

Provides access to the v2.1 api term store

+

Docs updated with v2.0.9 release as the underlying API changed

+
+

NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabilize this note will be removed.

+
+

Invokable Banner Selective Imports Banner

+

Batching Not Supported Banner

+

Term Store

+

Access term store data from the root sp object as shown below.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermStoreInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term store data
+const info: ITermStoreInfo = await sp.termStore();
+
+

searchTerm

+

Added in 3.3.0

+

Search for terms starting with provided label under entire termStore or a termSet or a parent term.

+

The following properties are valid for the supplied query: label: string, setId?: string, parentTermId?: string, languageTag?: string, stringMatchOption?: "ExactMatch" | "StartsWith".

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// minimally requires the label
+const results1 = await sp.termStore.searchTerm({
+  label: "test",
+});
+
+// other properties can be included as needed
+const results2 = await sp.termStore.searchTerm({
+  label: "test",
+  setId: "{guid}",
+});
+
+// other properties can be included as needed
+const results3 = await sp.termStore.searchTerm({
+  label: "test",
+  setId: "{guid}",
+  stringMatchOption: "ExactMatch",
+});
+
+

update

+

Added in 3.10.0

+

Allows you to update language setttings for the store

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+await sp.termStore.update({
+  defaultLanguageTag: "en-US",
+  languageTags: ["en-US", "en-IE", "de-DE"],
+});
+
+

Term Groups

+

Access term group information

+

List

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term groups
+const info: ITermGroupInfo[] = await sp.termStore.groups();
+
+

Get By Id

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term groups data
+const info: ITermGroupInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72")();
+
+

Add

+

Added in 3.10.0

+

Allows you to add a term group to a store.

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+const groupInfo: ITermGroupInfo = await sp.termStore.groups.add({
+  displayName: "Accounting",
+  description: "Term Group for Accounting",
+  name: "accounting1",
+  scope: "global",
+});
+
+

Term Group

+

Delete

+

Added in 3.10.0

+

Allows you to add a term group to a store.

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+

Term Sets

+

Access term set information

+

List

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermSetInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get set info
+const info: ITermSetInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets();
+
+

Get By Id

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermSetInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term set data by group id then by term set id
+const info: ITermSetInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72")();
+
+// get term set data by term set id
+const infoByTermSetId: ITermSetInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72")();
+
+

Add

+

Added in 3.10.0

+

Allows you to add a term set.

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+// when adding a set directly from the root .sets property, you must include the "parentGroup" property
+const setInfo = await sp.termStore.sets.add({
+  parentGroup: {
+    id: "338666a8-1111-2222-3333-f72471314e72"
+  },
+  contact: "steve",
+  description: "description",
+  isAvailableForTagging: true,
+  isOpen: true,
+  localizedNames: [{
+    name: "MySet",
+    languageTag: "en-US",
+  }],
+  properties: [{
+    key: "key1",
+    value: "value1",
+  }]
+});
+
+// when adding a termset through a group's sets property you do not specify the "parentGroup" property
+const setInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.add({
+  contact: "steve",
+  description: "description",
+  isAvailableForTagging: true,
+  isOpen: true,
+  localizedNames: [{
+    name: "MySet2",
+    languageTag: "en-US",
+  }],
+  properties: [{
+    key: "key1",
+    value: "value1",
+  }]
+});
+
+

getAllChildrenAsOrderedTree

+

This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change.

+
+

Starting with version 2.6.0 you can now include an optional param to retrieve all of the term's properties and localProperties in the tree. Default is false.

+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+import { dateAdd, PnPClientStorage } from "@pnp/core";
+
+const sp = spfi(...);
+
+// here we get all the children of a given set
+const childTree = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getAllChildrenAsOrderedTree();
+
+// here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available
+const store = new PnPClientStorage();
+
+// our tree likely doesn't change much in 30 minutes for most applications
+// adjust to be longer or shorter as needed
+const cachedTree = await store.local.getOrPut("myKey", () => {
+    return sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getAllChildrenAsOrderedTree();
+}, dateAdd(new Date(), "minute", 30));
+
+// you can also get all the properties and localProperties
+const set = sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72");
+const childTree = await set.getAllChildrenAsOrderedTree({ retrieveProperties: true });
+
+

TermSet

+

Access term set information

+

Update

+

Added in 3.10.0

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const termSetInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").update({
+  properties: [{
+    key: "MyKey2",
+    value: "MyValue2",
+  }],
+});
+
+const termSetInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").update({
+  properties: [{
+    key: "MyKey3",
+    value: "MyValue3",
+  }],
+});
+
+

Delete

+

Added in 3.10.0

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermGroupInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+

Terms

+

Access term set information

+

List

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// list all the terms that are direct children of this set
+const infos: ITermInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children();
+
+

List (terms)

+

You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// list all the terms available in this term set by group id then by term set id
+const infos: ITermInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").terms();
+
+// list all the terms available in this term set by term set id
+const infosByTermSetId: ITermInfo[] = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").terms();
+
+

Get By Id

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get term set data
+const info: ITermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72")();
+
+

Add

+

Added in 3.10.0

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+import { ITermInfo } from "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const newTermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children.add({
+  labels: [
+    {
+      isDefault: true,
+      languageTag: "en-us",
+      name: "New Term",
+    }
+  ]
+});
+
+const newTermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children.add({
+  labels: [
+    {
+      isDefault: true,
+      languageTag: "en-us",
+      name: "New Term 2",
+    }
+  ]
+});
+
+

Term

+

Update

+
+

Note that when updating a Term if you update the properties it replaces the collection, so a merge of existing + new needs to be handled by your application.

+
+

Added in 3.10.0

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const termInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").update({
+  properties: [{
+    key: "something",
+    value: "a value 2",
+  }],
+});
+
+const termInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").update({
+  properties: [{
+    key: "something",
+    value: "a value",
+  }],
+});
+
+

Delete

+

Added in 3.10.0

+
import { spfi, SPFxToken, SPFx } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+// NOTE: Because this endpoint requires a token and does not work with cookie auth you must create an instance of SPFI that includes an auth token.
+// We've included a new behavior to support getting a token for sharepoint called `SPFxToken`
+const sp = spfi().using(SPFx(context), SPFxToken(context));
+
+const termInfo = await sp.termStore.sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+const termInfo2 = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72").delete();
+
+

Get Term Parent

+

Behavior Change in 2.1.0

+

The server API changed again, resulting in the removal of the "parent" property from ITerm as it is not longer supported as a path property. You now must use "expand" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/taxonomy";
+
+const sp = spfi(...);
+
+// get a ref to the set
+const set = sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72");
+
+// get a term's information and expand parent to get the parent info as well
+const w = await set.getTermById("338666a8-1111-2222-3333-f72471314e72").expand("parent")();
+
+// get a ref to the parent term
+const parent = set.getTermById(w.parent.id);
+
+// make a request for the parent term's info - this data currently match the results in the expand call above, but this
+// is to demonstrate how to gain a ref to the parent and select its data
+const parentInfo = await parent.select("Id", "Descriptions")();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/tenant-properties/index.html b/sp/tenant-properties/index.html new file mode 100644 index 000000000..5097d89ee --- /dev/null +++ b/sp/tenant-properties/index.html @@ -0,0 +1,2732 @@ + + + + + + + + + + + + + + + + + + + + + + + + Tenant Properties - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/web - tenant properties

+

You can set, read, and remove tenant properties using the methods shown below:

+

setStorageEntity

+

This method MUST be called in the context of the app catalog web or you will get an access denied message.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const w = await sp.getTenantAppCatalogWeb();
+
+// specify required key and value
+await w.setStorageEntity("Test1", "Value 1");
+
+// specify optional description and comments
+await w.setStorageEntity("Test2", "Value 2", "description", "comments");
+
+

getStorageEntity

+

This method can be used from any web to retrieve values previously set.

+
import { spfi, SPFx } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+import { IStorageEntity } from "@pnp/sp/webs"; 
+
+const sp = spfi(...);
+
+const prop: IStorageEntity = await sp.web.getStorageEntity("Test1");
+
+console.log(prop.Value);
+
+

removeStorageEntity

+

This method MUST be called in the context of the app catalog web or you will get an access denied message.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/appcatalog";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const w = await sp.getTenantAppCatalogWeb();
+
+await w.removeStorageEntity("Test1");
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/user-custom-actions/index.html b/sp/user-custom-actions/index.html new file mode 100644 index 000000000..71234331a --- /dev/null +++ b/sp/user-custom-actions/index.html @@ -0,0 +1,2845 @@ + + + + + + + + + + + + + + + + + + + + + + + + User custom actions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + + + + + +
+
+ + + + + + + + +

@pnp/sp/user-custom-actions

+

Represents a custom action associated with a SharePoint list, web or site collection.

+

IUserCustomActions

+

Invokable Banner Selective Imports Banner

+

Get a collection of User Custom Actions from a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+const userCustomActions = sp.web.userCustomActions();
+
+

Add a new User Custom Action

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions';
+
+const sp = spfi(...);
+
+const newValues: TypedHash<string> = {
+    "Title": "New Title",
+    "Description": "New Description",
+    "Location": "ScriptLink",
+    "ScriptSrc": "https://..."
+};
+
+const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);
+
+

Get a User Custom Action by ID

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+const uca: IUserCustomAction = sp.web.userCustomActions.getById("00000000-0000-0000-0000-000000000000");
+
+const ucaData = await uca();
+
+

Clear the User Custom Action collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+// Site collection level
+await sp.site.userCustomActions.clear();
+
+// Site (web) level
+await sp.web.userCustomActions.clear();
+
+// List level
+await sp.web.lists.getByTitle("Documents").userCustomActions.clear();
+
+

IUserCustomAction

+

Update an existing User Custom Action

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions";
+import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions';
+
+const sp = spfi(...);
+
+const uca = sp.web.userCustomActions.getById("00000000-0000-0000-0000-000000000000");
+
+const newValues: TypedHash<string> = {
+    "Title": "New Title",
+    "Description": "New Description",
+    "ScriptSrc": "https://..."
+};
+
+const response: IUserCustomActionUpdateResult = uca.update(newValues);
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/views/index.html b/sp/views/index.html new file mode 100644 index 000000000..6ec1fcc34 --- /dev/null +++ b/sp/views/index.html @@ -0,0 +1,3089 @@ + + + + + + + + + + + + + + + + + + + + + + + + Views - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+
+
+ + + +
+
+
+ + + +
+
+ + + + + + + + +

@pnp/sp/views

+

Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.

+

IViews

+

Invokable Banner Selective Imports Banner

+

Get views in a list

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+// get all the views and their properties
+const views1 = await list.views();
+
+// you can use odata select operations to get just a set a fields
+const views2 = await list.views.select("Id", "Title")();
+
+// get the top three views
+const views3 = await list.views.top(3)();
+
+

Add a View

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+// create a new view with default fields and properties
+const result = await list.views.add("My New View");
+
+// create a new view with specific properties
+const result2 = await list.views.add("My New View 2", false, {
+    RowLimit: 10,
+    ViewQuery: "<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>",
+});
+
+// manipulate the view's fields
+await result2.view.fields.removeAll();
+
+await Promise.all([
+    result2.view.fields.add("Title"),
+    result2.view.fields.add("Modified"),
+]);
+
+

IView

+

Get a View's Information

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+const result = await list.views.getById("{GUID view id}")();
+
+const result2 = await list.views.getByTitle("My View")();
+
+const result3 = await list.views.getByTitle("My View").select("Id", "Title")();
+
+const result4 = await list.defaultView();
+
+const result5 = await list.getView("{GUID view id}")();
+
+

fields

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+const result = await list.views.getById("{GUID view id}").fields();
+
+

update

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const list = sp.web.lists.getByTitle("My List");
+
+const result = await list.views.getById("{GUID view id}").update({
+    RowLimit: 20,
+});
+
+

renderAsHtml

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const result = await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").renderAsHtml();
+
+

setViewXml

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const viewXml = "...";
+
+await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").setViewXml(viewXml);
+
+

delete

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const viewXml = "...";
+
+await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").delete();
+
+

ViewFields

+

getSchemaXml

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+const xml = await sp.web.lists.getByTitle("My List").defaultView.fields.getSchemaXml();
+
+

add

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.add("Created");
+
+

move

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.move("Created", 0);
+
+

remove

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.remove("Created");
+
+

removeAll

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/views";
+
+const sp = spfi(...);
+
+await sp.web.lists.getByTitle("My List").defaultView.fields.removeAll();
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/sp/webs/index.html b/sp/webs/index.html new file mode 100644 index 000000000..06211d144 --- /dev/null +++ b/sp/webs/index.html @@ -0,0 +1,4142 @@ + + + + + + + + + + + + + + + + + + + + + + + + Webs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

@pnp/sp/webs

+

Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.

+

IWebs

+

Invokable Banner Selective Imports Banner

+

Add Web

+

Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions.

+
import { spfi } from "@pnp/sp";
+import { IWebAddResult } from "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const result = await sp.web.webs.add("title", "subweb1");
+
+// show the response from the server when adding the web
+console.log(result.data);
+
+// we can immediately operate on the new web
+result.web.select("Title")().then((w: IWebInfo)  => {
+
+    // show our title
+    console.log(w.Title);
+});
+
+
import { spfi } from "@pnp/sp";
+import { IWebAddResult } from "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// create a German language wiki site with title, url, description, which does not inherit permissions
+sp.web.webs.add("wiki", "subweb2", "a wiki web", "WIKI#0", 1031, false).then((w: IWebAddResult) => {
+
+  // ...
+});
+
+

IWeb

+

Invokable Banner Selective Imports Banner

+

Access a Web

+

There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named "web" which represents an IWeb instance - regardless of how it was initially accessed.

+

Access the web from the imported "spfi" object using selective import:

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const r = await sp.web();
+
+

Access the web from the imported "spfi" object using the 'all' preset

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/presets/all";
+
+const sp = spfi(...);
+
+const r = await sp.web();
+
+

Access the web using any SPQueryable as a base

+

In this scenario you might be deep in your code without access to the original start of the fluid chain (i.e. the instance produced from spfi). You can pass any queryable to the Web or Site factory and get back a valid IWeb instance. In this case all of the observers registered to the supplied instance will be referenced by the IWeb, and the url will be rebased to ensure a valid path.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists";
+import "@pnp/sp/items";
+
+const sp = spfi(...);
+
+// we have a ref to the IItems instance
+const items = await sp.web.lists.getByTitle("Generic").items;
+
+// we create a new IWeb instance using the items as a base
+const web = Web(items);
+
+// gets the web info
+const webInfo = await web();
+
+// get a reference to a different list
+const list = web.lists.getByTitle("DifferentList");
+
+

Access a web using the Web factory method

+

There are several ways to use the Web factory directly and have some special considerations unique to creating IWeb instances from Web. The easiest is to supply the absolute URL of the web you wish to target, as seen in the first example below. When supplying a path parameter to Web you need to include the _api/web part in the appropriate location as the library can't from strings determine how to append the path. Example 2 below shows a wrong usage of the Web factory as we cannot determine how the path part should be appended. Examples 3 and 4 show how to include the _api/web part for both subwebs or queries within the given web.

+
+

When in doubt, supply the absolute url to the web as the first parameter as shown in example 1 below

+
+
import { spfi } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+// creates a web:
+// - whose root is "https://tenant.sharepoint.com/sites/myweb"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/_api/web"
+// - has no registered observers
+const web1 = Web("https://tenant.sharepoint.com/sites/myweb");
+
+// creates a web that will not work due to missing the _api/web portion
+// this is because we don't know that the extra path should come before/after the _api/web portion
+// - whose root is "https://tenant.sharepoint.com/sites/myweb/some sub path"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/some sub path"
+// - has no registered observers
+const web2-WRONG = Web("https://tenant.sharepoint.com/sites/myweb", "some sub path");
+
+// creates a web:
+// - whose root is "https://tenant.sharepoint.com/sites/myweb/some sub path"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/some sub web/_api/web"
+// including the _api/web ensures the path you are providing is correct and can be parsed by the library
+// - has no registered observers
+const web3 = Web("https://tenant.sharepoint.com/sites/myweb", "some sub web/_api/web");
+
+// creates a web that actually points to the lists endpoint:
+// - whose root is "https://tenant.sharepoint.com/sites/myweb/"
+// - whose request path is "https://tenant.sharepoint.com/sites/myweb/_api/web/lists"
+// including the _api/web ensures the path you are providing is correct and can be parsed by the library
+// - has no registered observers
+const web4 = Web("https://tenant.sharepoint.com/sites/myweb", "_api/web/lists");
+
+

The above examples show you how to use the constructor to create the base url for the Web although none of them are usable as is until you add observers. You can do so by either adding them explicitly with a using...

+
import { spfi, SPFx } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+const web1 = Web("https://tenant.sharepoint.com/sites/myweb").using(SPFx(this.context));
+
+

or by copying them from another SPQueryable instance...

+
import { spfi } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+//sp.web is of type SPQueryable; using tuple pattern pass SPQueryable and the web's url
+const web = Web([sp.web, "https://tenant.sharepoint.com/sites/otherweb"]);
+
+

webs

+

Access the child webs collection of this web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const web = sp.web;
+const webs = await web.webs();
+
+

Get A Web's properties

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+// basic get of the webs properties
+const props = await sp.web();
+
+// use odata operators to get specific fields
+const props2 = await sp.web.select("Title")();
+
+// type the result to match what you are requesting
+const props3 = await sp.web.select("Title")<{ Title: string }>();
+
+

getParentWeb

+

Get the data and IWeb instance for the parent web for the given web instance

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = web.getParentWeb();
+
+

getSubwebsFilteredForCurrentUser

+

Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const web = sp.web;
+const subWebs = web.getSubwebsFilteredForCurrentUser()();
+
+// apply odata operations to the collection
+const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select("Title", "Language").orderBy("Created", true)();
+
+
+

Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.

+
+

allProperties

+

Allows access to the web's all properties collection. This is readonly in REST.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+
+const web = sp.web;
+const props = await web.allProperties();
+
+// select certain props
+const props2 = await web.allProperties.select("prop1", "prop2")();
+
+

webinfos

+

Gets a collection of WebInfos for this web's subwebs

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+
+const infos = await web.webinfos();
+
+// or select certain fields
+const infos2 = await web.webinfos.select("Title", "Description")();
+
+// or filter
+const infos3 = await web.webinfos.filter("Title eq 'MyWebTitle'")();
+
+// or both
+const infos4 = await web.webinfos.select("Title", "Description").filter("Title eq 'MyWebTitle'")();
+
+// get the top 4 ordered by Title
+const infos5 = await web.webinfos.top(4).orderBy("Title")();
+
+
+

Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.

+
+

update

+

Updates this web instance with the supplied properties

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+// update the web's title and description
+const result = await web.update({
+    Title: "New Title",
+    Description: "My new description",
+});
+
+// a project implementation could wrap the update to provide type information for your expected fields:
+
+interface IWebUpdateProps {
+    Title: string;
+    Description: string;
+}
+
+function updateWeb(props: IWebUpdateProps): Promise<void> {
+    web.update(props);
+}
+
+

Delete a Web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+
+await web.delete();
+
+

applyTheme

+

Applies the theme specified by the contents of each of the files specified in the arguments to the site

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { combine } from "@pnp/core";
+
+const sp = spfi("https://{tenant}.sharepoint.com/sites/dev/subweb").using(SPFx(this.context));
+const web = sp.web;
+
+// the urls to the color and font need to both be from the catalog at the root
+// these urls can be constants or calculated from existing urls
+const colorUrl =  combine("/", "sites/dev", "_catalogs/theme/15/palette011.spcolor");
+// this gives us the same result
+const fontUrl = "/sites/dev/_catalogs/theme/15/fontscheme007.spfont";
+
+// apply the font and color, no background image, and don't share this theme
+await web.applyTheme(colorUrl, fontUrl, "", false);
+
+

applyWebTemplate & availableWebTemplates

+

Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+const templates = (await web.availableWebTemplates().select("Name")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name));
+
+// apply the wiki template
+const template = templates.length > 0 ? templates[0].Name : "STS#0";
+
+await web.applyWebTemplate(template);
+
+

getChanges

+

Returns the collection of changes from the change log that have occurred within the web, based on the specified query.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+
+const sp = spfi(...);
+const web = sp.web;
+// get the web changes including add, update, and delete
+const changes = await web.getChanges({
+        Add: true,
+        ChangeTokenEnd: undefined,
+        ChangeTokenStart: undefined,
+        DeleteObject: true,
+        Update: true,
+        Web: true,
+    });
+
+

mapToIcon

+

Returns the name of the image file for the icon that is used to represent the specified file

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { combine } from "@pnp/core";
+
+const iconFileName = await web.mapToIcon("test.docx");
+// iconPath === "icdocx.png"
+// which you can need to map to a real url
+const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`;
+
+// OR dynamically
+const sp = spfi(...);
+const webData = await sp.web.select("Url")();
+const iconFullPath2 = combine(webData.Url, "_layouts", "images", iconFileName);
+
+// OR within SPFx using the context
+const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, "_layouts", "images", iconFileName);
+
+// You can also set size
+// 16x16 pixels = 0, 32x32 pixels = 1
+const icon32FileName = await web.mapToIcon("test.docx", 1);
+
+

storage entities

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/appcatalog";
+import { IStorageEntity } from "@pnp/sp/webs";
+
+// needs to be unique, GUIDs are great
+const key = "my-storage-key";
+
+const sp = spfi(...);
+
+// read an existing entity
+const entity: IStorageEntity = await sp.web.getStorageEntity(key);
+
+// setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site
+// you can get the tenant app catalog using the getTenantAppCatalogWeb
+const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb();
+
+tenantAppCatalogWeb.setStorageEntity(key, "new value");
+
+// set other properties
+tenantAppCatalogWeb.setStorageEntity(key, "another value", "description", "comments");
+
+const entity2: IStorageEntity = await sp.web.getStorageEntity(key);
+/*
+entity2 === {
+    Value: "another value",
+    Comment: "comments";
+    Description: "description",
+};
+*/
+
+// you can also remove a storage entity
+await tenantAppCatalogWeb.removeStorageEntity(key);
+
+

getAppCatalog

+

Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IApp } from "@pnp/sp/appcatalog";
+
+const sp = spfi(...);
+
+const appWeb = sp.web.appcatalog;
+const app: IApp = appWeb.getAppById("{your app id}");
+// appWeb url === web url
+
+

client-side-pages

+

You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/clientside-pages/web";
+
+const sp = spfi(...);
+
+// simplest add a page example
+const page = await sp.web.addClientsidePage("mypage1");
+
+// simplest load a page example
+const page = await sp.web.loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
+
+

contentTypes

+

Allows access to the collection of content types in this web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/content-types/web";
+
+const sp = spfi(...);
+
+const cts = await sp.web.contentTypes();
+
+// you can also select fields and use other odata operators
+const cts2 = await sp.web.contentTypes.select("Name")();
+
+

features

+

Allows access to the collection of content types in this web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/features/web";
+
+const sp = spfi(...);
+
+const features = await sp.web.features();
+
+

fields

+

Allows access to the collection of fields in this web.

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/fields/web";
+
+const sp = spfi(...);
+const fields = await sp.web.fields();
+
+

getFileByServerRelativePath

+

Gets a file by server relative url if your file name contains # and % characters

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/files/web";
+import { IFile } from "@pnp/sp/files/types";
+
+const sp = spfi(...);
+const file: IFile = web.getFileByServerRelativePath("/sites/dev/library/my # file%.docx");
+
+

folders

+

Gets the collection of folders in this web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+
+const sp = spfi(...);
+
+const folders = await sp.web.folders();
+
+// you can also filter and select as with any collection
+const folders2 = await sp.web.folders.select("ServerRelativeUrl", "TimeLastModified").filter("ItemCount gt 0")();
+
+// or get the most recently modified folder
+const folders2 = await sp.web.folders.orderBy("TimeLastModified").top(1)();
+
+

rootFolder

+

Gets the root folder of the web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+
+const sp = spfi(...);
+
+const folder = await sp.web.rootFolder();
+
+

getFolderByServerRelativePath

+

Gets a folder by server relative url if your folder name contains # and % characters

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/folders/web";
+import { IFolder } from "@pnp/sp/folders";
+
+const sp = spfi(...);
+
+const folder: IFolder = web.getFolderByServerRelativePath("/sites/dev/library/my # folder%/");
+
+

hubSiteData

+

Gets hub site data for the current web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+// get the data and force a refresh
+const data = await sp.web.hubSiteData(true);
+
+

syncHubSiteTheme

+

Applies theme updates from the parent hub site collection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/hubsites/web";
+
+const sp = spfi(...);
+await sp.web.syncHubSiteTheme();
+
+

lists

+

Gets the collection of all lists that are contained in the Web site

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { ILists } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const lists: ILists = sp.web.lists;
+
+// you can always order the lists and select properties
+const data = await lists.select("Title").orderBy("Title")();
+
+// and use other odata operators as well
+const data2 = await sp.web.lists.top(3).orderBy("LastItemModifiedDate")();
+
+

siteUserInfoList

+

Gets the UserInfo list of the site collection that contains the Web site

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { IList } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const list: IList = sp.web.siteUserInfoList;
+
+const data = await list();
+
+// or chain off that list to get additional details
+const items = await list.items.top(2)();
+
+

defaultDocumentLibrary

+

Get a reference to the default document library of a web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists/web";
+
+const sp = spfi(...);
+const list: IList = sp.web.defaultDocumentLibrary;
+
+

customListTemplates

+

Gets the collection of all list definitions and list templates that are available

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/lists/web";
+import { IList } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const templates = await sp.web.customListTemplates();
+
+// odata operators chain off the collection as expected
+const templates2 = await sp.web.customListTemplates.select("Title")();
+
+

getList

+

Gets a list by server relative url (list's root folder)

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists/web";
+
+const sp = spfi(...);
+const list: IList = sp.web.getList("/sites/dev/lists/test");
+
+const listData = await list();
+
+

getCatalog

+

Returns the list gallery on the site

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameValue
WebTemplateCatalog111
WebPartCatalog113
ListTemplateCatalog114
MasterPageCatalog116
SolutionCatalog121
ThemeCatalog123
DesignCatalog124
AppDataCatalog125
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import { IList } from "@pnp/sp/lists";
+
+const sp = spfi(...);
+const templateCatalog: IList = await sp.web.getCatalog(111);
+
+const themeCatalog: IList = await sp.web.getCatalog(123);
+
+ +

Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation/web";
+import { INavigation } from "@pnp/sp/navigation";
+
+const sp = spfi(...);
+const nav: INavigation = sp.web.navigation;
+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/navigation/web";
+import { IRegionalSettings } from "@pnp/sp/navigation";
+
+const sp = spfi(...);
+const settings: IRegionalSettings = sp.web.regionalSettings;
+
+const settingsData = await settings();
+
+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/related-items/web";
+import { IRelatedItemManager, IRelatedItem } from "@pnp/sp/related-items";
+
+const sp = spfi(...);
+const manager: IRelatedItemManager = sp.web.relatedItems;
+
+const data: IRelatedItem[] = await manager.getRelatedItems("{list name}", 4);
+
+

security imports

+

Please see information around the available security methods in the security article.

+

sharing imports

+

Please see information around the available sharing methods in the sharing article.

+

siteGroups

+

The site groups

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+const groups = await sp.web.siteGroups();
+
+const groups2 = await sp.web.siteGroups.top(2)();
+
+

associatedOwnerGroup

+

The web's owner group

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+const group = await sp.web.associatedOwnerGroup();
+
+const users = await sp.web.associatedOwnerGroup.users();
+
+

associatedMemberGroup

+

The web's member group

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+const group = await sp.web.associatedMemberGroup();
+
+const users = await sp.web.associatedMemberGroup.users();
+
+

associatedVisitorGroup

+

The web's visitor group

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+const group = await sp.web.associatedVisitorGroup();
+
+const users = await sp.web.associatedVisitorGroup.users();
+
+

createDefaultAssociatedGroups

+

Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-groups/web";
+
+const sp = spfi(...);
+
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}");
+
+// copy the role assignments
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}", true);
+
+// don't clear sub assignments
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, false);
+
+// specify secondary owner, don't copy permissions, clear sub scopes
+await sp.web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, true, "{second owner login}");
+
+

siteUsers

+

The site users

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const users = await sp.web.siteUsers();
+
+const users2 = await sp.web.siteUsers.top(5)();
+
+const users3 = await sp.web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent("i:0#.f|m")}')`)();
+
+

currentUser

+

Information on the current user

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+
+const sp = spfi(...);
+
+const user = await sp.web.currentUser();
+
+// check the login name of the current user
+const user2 = await sp.web.currentUser.select("LoginName")();
+
+

ensureUser

+

Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { IWebEnsureUserResult } from "@pnp/sp/site-users/";
+
+const sp = spfi(...);
+
+const result: IWebEnsureUserResult = await sp.web.ensureUser("i:0#.f|membership|user@domain.onmicrosoft.com");
+
+

getUserById

+

Returns the user corresponding to the specified member identifier for the current web

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/site-users/web";
+import { ISiteUser } from "@pnp/sp/site-users/";
+
+const sp = spfi(...);
+
+const user: ISiteUser = sp.web.getUserById(23);
+
+const userData = await user();
+
+const userData2 = await user.select("LoginName")();
+
+

userCustomActions

+

Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection

+
import { spfi } from "@pnp/sp";
+import "@pnp/sp/webs";
+import "@pnp/sp/user-custom-actions/web";
+import { IUserCustomActions } from "@pnp/sp/user-custom-actions";
+
+const sp = spfi(...);
+
+const actions: IUserCustomActions = sp.web.userCustomActions;
+
+const actionsData = await actions();
+
+

IWebInfosData

+

Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations.

+
interface IWebInfosData {
+    Configuration: number;
+    Created: string;
+    Description: string;
+    Id: string;
+    Language: number;
+    LastItemModifiedDate: string;
+    LastItemUserModifiedDate: string;
+    ServerRelativeUrl: string;
+    Title: string;
+    WebTemplate: string;
+    WebTemplateId: number;
+}
+
+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/transition-guide/index.html b/transition-guide/index.html new file mode 100644 index 000000000..67dcfcfc6 --- /dev/null +++ b/transition-guide/index.html @@ -0,0 +1,2796 @@ + + + + + + + + + + + + + + + + + + + + + + + + V2->V3 Transition - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + +
+ + +
+ +
+ + + + + + +
+
+ + + +
+
+
+ + + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + + + + + + +

Transition Guide

+

It is our hope that the transition from version 2.* to 3.* will be as painless as possible, however given the transition we have made from a global sp object to an instance based object some architectural and inital setup changes will need to be addressed. In the following sections we endevor to provide an overview of what changes will be required. If we missed something, please let us know in the issues list so we can update the guide. Thanks!

+

For a full, detailed list of what's been added, updated, and removed please see our CHANGELOG

+

For a full sample project, utilizing SPFx 1.14 and V3 that showcases some of the more dramatic changes to the library check out this sample.

+

Benefits and Advancements in V3

+

For version 2 the core themes were selective imports, a model based on factory functions & interfaces, and improving the docs. This foundation gave us the opportunity to re-write the entire request pipeline internals with minimal external library changes - showing a bit of long-term planning 🙂. With version 3 your required updates are likely to only affect the initial configuration of the library, a huge accomplishment when updating the entire internals.

+

Our request pipeline remained largely unchanged since it was first written ~5 years ago, hard to change something so central to the library. The advantage of this update it is now easy for developers to inject their own logic into the request process. As always, this work was based on feedback over the years and understanding how we can be a better library. The new observer design allows you to customize every aspect of the request, in a much clearer way than was previously possible. In addition this work greatly reduced internal boilerplate code and optimized for library size. We reduced the size of sp & graph libraries by almost 2/3. As well we embraced a fully async design built around the new Timeline. Check out the new model around authoring observers and understand how they relate to moments. We feel this new architecture will allow far greater flexibility for consumers of the library to customize the behavior to exactly meet their needs.

+

We also used this as an opportunity to remove duplicate methods, clean up and improve our typings & method signatures, and drop legacy methods. Be sure to review the changelog. As always we do our best to minimize breaking changes but major versions are breaking versions.

+

We thank you for using the library. Your continued feedback drives these improvements, and we look forward to seeing what you build!

+

Global vs Instance Architecture

+

The biggest change in version 3 of the library is the movement away from the globally defined sp and graph objects. Starting in version 2.1.0 we added the concept of Isolated Runtime which allowed you to create a separate instance of the global object that would have a separate configuration. We found that the implementation was finicky and prone to issues, so we have rebuilt the internals of the library from the ground up to better address this need. In doing so, we decided not to offer a global object at all.

+

Because of this change, any architecture that relies on the sp or graph objects being configured during initialization and then reused throughout the solution will need to be rethought. Essentially you have three options:

+
    +
  1. Create a new spfi/graphfi object wherever it's required.
  2. +
  3. Create a service architecture that can return a previously configured instance or utilize an instance and return the results
  4. +
  5. Utilize a Project Preset file.
  6. +
+

In other words, the sp and graph objects have been deprecated and will need to be replaced.

+

For more information on getting started with these new setup methods please see the Getting Started docs for a deeper look into the Queryable interface see Queryable.

+

AssignFrom and CopyFrom

+

With the new Querable instance architecture we have provided a way to branch from one instance to another. To do this we provide two methods: AssignFrom and CopyFrom. These methods can be helpful when you want to establish a new instance to which you might apply other behaviors but want to reuse the configuration from a source. To learn more about them check out the Core/Behaviors documentation.

+

Dropping ".get()"

+

If you are still using the queryableInstance.get() method of queryable you must replace it with a direct invoke call queryableInstance().

+

Batching

+

Another benefit of the new updated internals is a significantly streamlined and simplified process for batching requests. Essentially, the interface for SP and Graph now function the same.

+

A new module called "batching" will need to be imported which then provides the batched interface which will return a tuple with a new Querable instance and an execute function. To see more details check out Batching.

+

Web -> SPFI

+

In V2, to connect to a different web you would use the function

+
const web = Web({Other Web URL});
+
+

In V3 you would create a new instance of queryable connecting to the web of your choice. This new method provides you significantly more flexibility by not only allowing you to easily connect to other webs in the same tenant but also to webs in other tenants.

+

We are seeing a significant number of people report an error when using this method:

+

No observers registered for this request.

+

which results when it hasn't been updated to use the version 3 convention. Please see the examples below to pick the one that most suits your codebase.

+
import { spfi, SPFx } from "@pnp/sp";
+import { Web } from "@pnp/sp/webs";
+
+const spWebA = spfi().using(SPFx(this.context));
+
+// Easiest transition is to use the tuple pattern and the Web constructor which will copy all the observers from the object but set the url to the one provided
+const spWebE = Web([spWebA.web, "{Absolute URL of Other Web}"]);
+
+// Create a new instance of Queryable
+const spWebB = spfi("{Other Web URL}").using(SPFx(this.context));
+
+// Copy/Assign a new instance of Queryable using the existing
+const spWebC = spfi("{Other Web URL}").using(AssignFrom(sp.web));
+
+// Create a new instance of Queryable using other credentials?
+const spWebD = spfi("{Other Web URL}").using(SPFx(this.context));
+
+
+

Please see the documentation for more information on the updated Web constructor.

+

Dropping -Commonjs Packages

+

Starting with v3 we are dropping the commonjs versions of all packages. Previously we released these as we worked to transition to esm and the current node didn't yet support esm. With esm now a supported module type, and having done the work to ensure they work in node we feel it is a good time to drop the -commonjs variants. Please see documentation on Getting started with NodeJS Project using TypeScript producing CommonJS modules

+ + + + + + +
+
+ + +
+ +
+ + + +
+
+
+
+ + + + + + + + + \ No newline at end of file diff --git a/v1/404.html b/v1/404.html new file mode 100644 index 000000000..ffa3a384e --- /dev/null +++ b/v1/404.html @@ -0,0 +1,1611 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + +
+
+ +

404 - Not found

+ + + + + + +
+
+
+
+ + + + +
+ + + + + + + + + + + \ No newline at end of file diff --git a/v1/assets/fonts/font-awesome.css b/v1/assets/fonts/font-awesome.css new file mode 100644 index 000000000..b476b53e3 --- /dev/null +++ b/v1/assets/fonts/font-awesome.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:FontAwesome;font-style:normal;font-weight:400;src:url("specimen/FontAwesome.woff2") format("woff2"),url("specimen/FontAwesome.woff") format("woff"),url("specimen/FontAwesome.ttf") format("truetype")}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1,1);-ms-transform:scale(-1,1);transform:scale(-1,1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1,-1);-ms-transform:scale(1,-1);transform:scale(1,-1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} \ No newline at end of file diff --git a/v1/assets/fonts/material-icons.css b/v1/assets/fonts/material-icons.css new file mode 100644 index 000000000..d23d365ed --- /dev/null +++ b/v1/assets/fonts/material-icons.css @@ -0,0 +1,13 @@ +/*! + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING, SOFTWARE + * DISTRIBUTED UNDER THE LICENSE IS DISTRIBUTED ON AN "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED. + * SEE THE LICENSE FOR THE SPECIFIC LANGUAGE GOVERNING PERMISSIONS AND + * LIMITATIONS UNDER THE LICENSE. + */@font-face{font-family:"Material Icons";font-style:normal;font-weight:400;src:local("Material Icons"),local("MaterialIcons-Regular"),url("specimen/MaterialIcons-Regular.woff2") format("woff2"),url("specimen/MaterialIcons-Regular.woff") format("woff"),url("specimen/MaterialIcons-Regular.ttf") format("truetype")} \ No newline at end of file diff --git a/v1/assets/fonts/specimen/FontAwesome.ttf b/v1/assets/fonts/specimen/FontAwesome.ttf new file mode 100644 index 000000000..35acda2fa Binary files /dev/null and b/v1/assets/fonts/specimen/FontAwesome.ttf differ diff --git a/v1/assets/fonts/specimen/FontAwesome.woff b/v1/assets/fonts/specimen/FontAwesome.woff new file mode 100644 index 000000000..400014a4b Binary files /dev/null and b/v1/assets/fonts/specimen/FontAwesome.woff differ diff --git a/v1/assets/fonts/specimen/FontAwesome.woff2 b/v1/assets/fonts/specimen/FontAwesome.woff2 new file mode 100644 index 000000000..4d13fc604 Binary files /dev/null and b/v1/assets/fonts/specimen/FontAwesome.woff2 differ diff --git a/v1/assets/fonts/specimen/MaterialIcons-Regular.ttf b/v1/assets/fonts/specimen/MaterialIcons-Regular.ttf new file mode 100644 index 000000000..7015564ad Binary files /dev/null and b/v1/assets/fonts/specimen/MaterialIcons-Regular.ttf differ diff --git a/v1/assets/fonts/specimen/MaterialIcons-Regular.woff b/v1/assets/fonts/specimen/MaterialIcons-Regular.woff new file mode 100644 index 000000000..b648a3eea Binary files /dev/null and b/v1/assets/fonts/specimen/MaterialIcons-Regular.woff differ diff --git a/v1/assets/fonts/specimen/MaterialIcons-Regular.woff2 b/v1/assets/fonts/specimen/MaterialIcons-Regular.woff2 new file mode 100644 index 000000000..9fa211252 Binary files /dev/null and b/v1/assets/fonts/specimen/MaterialIcons-Regular.woff2 differ diff --git a/v1/assets/images/favicon.png b/v1/assets/images/favicon.png new file mode 100644 index 000000000..76d17f57a Binary files /dev/null and b/v1/assets/images/favicon.png differ diff --git a/v1/assets/images/icons/bitbucket.1b09e088.svg b/v1/assets/images/icons/bitbucket.1b09e088.svg new file mode 100644 index 000000000..a25435af3 --- /dev/null +++ b/v1/assets/images/icons/bitbucket.1b09e088.svg @@ -0,0 +1,20 @@ + + + diff --git a/v1/assets/images/icons/github.f0b8504a.svg b/v1/assets/images/icons/github.f0b8504a.svg new file mode 100644 index 000000000..c009420a7 --- /dev/null +++ b/v1/assets/images/icons/github.f0b8504a.svg @@ -0,0 +1,18 @@ + + + diff --git a/v1/assets/images/icons/gitlab.6dd19c00.svg b/v1/assets/images/icons/gitlab.6dd19c00.svg new file mode 100644 index 000000000..9e3d6f05b --- /dev/null +++ b/v1/assets/images/icons/gitlab.6dd19c00.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/v1/assets/javascripts/application.583bbe55.js b/v1/assets/javascripts/application.583bbe55.js new file mode 100644 index 000000000..7cc20810e --- /dev/null +++ b/v1/assets/javascripts/application.583bbe55.js @@ -0,0 +1 @@ +!function(e,t){for(var n in t)e[n]=t[n]}(window,function(e){function t(r){if(n[r])return n[r].exports;var i=n[r]={i:r,l:!1,exports:{}};return e[r].call(i.exports,i,i.exports,t),i.l=!0,i.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=6)}([function(e,t,n){"use strict";t.__esModule=!0,t.default={createElement:function(e,t){var n=document.createElement(e);t&&Array.prototype.forEach.call(Object.keys(t),function(e){n.setAttribute(e,t[e])});for(var r=arguments.length,i=Array(r>2?r-2:0),o=2;o pre, pre > code");Array.prototype.forEach.call(n,function(t,n){var r="__code_"+n,i=e.createElement("button",{class:"md-clipboard",title:h("clipboard.copy"),"data-clipboard-target":"#"+r+" pre, #"+r+" code"},e.createElement("span",{class:"md-clipboard__message"})),o=t.parentNode;o.id=r,o.insertBefore(i,t)});new c.default(".md-clipboard").on("success",function(e){var t=e.trigger.querySelector(".md-clipboard__message");if(!(t instanceof HTMLElement))throw new ReferenceError;e.clearSelection(),t.dataset.mdTimer&&clearTimeout(parseInt(t.dataset.mdTimer,10)),t.classList.add("md-clipboard__message--active"),t.innerHTML=h("clipboard.copied"),t.dataset.mdTimer=setTimeout(function(){t.classList.remove("md-clipboard__message--active"),t.dataset.mdTimer=""},2e3).toString()})}if(!Modernizr.details){var r=document.querySelectorAll("details > summary");Array.prototype.forEach.call(r,function(e){e.addEventListener("click",function(e){var t=e.target.parentNode;t.hasAttribute("open")?t.removeAttribute("open"):t.setAttribute("open","")})})}var i=function(){if(document.location.hash){var e=document.getElementById(document.location.hash.substring(1));if(!e)return;for(var t=e.parentNode;t&&!(t instanceof HTMLDetailsElement);)t=t.parentNode;if(t&&!t.open){t.open=!0;var n=location.hash;location.hash=" ",location.hash=n}}};if(window.addEventListener("hashchange",i),i(),Modernizr.ios){var o=document.querySelectorAll("[data-md-scrollfix]");Array.prototype.forEach.call(o,function(e){e.addEventListener("touchstart",function(){var t=e.scrollTop;0===t?e.scrollTop=1:t+e.offsetHeight===e.scrollHeight&&(e.scrollTop=t-1)})})}}).listen(),new f.default.Event.Listener(window,["scroll","resize","orientationchange"],new f.default.Header.Shadow("[data-md-component=container]","[data-md-component=header]")).listen(),new f.default.Event.Listener(window,["scroll","resize","orientationchange"],new f.default.Header.Title("[data-md-component=title]",".md-typeset h1")).listen(),document.querySelector("[data-md-component=hero]")&&new f.default.Event.Listener(window,["scroll","resize","orientationchange"],new f.default.Tabs.Toggle("[data-md-component=hero]")).listen(),document.querySelector("[data-md-component=tabs]")&&new f.default.Event.Listener(window,["scroll","resize","orientationchange"],new f.default.Tabs.Toggle("[data-md-component=tabs]")).listen(),new f.default.Event.MatchMedia("(min-width: 1220px)",new f.default.Event.Listener(window,["scroll","resize","orientationchange"],new f.default.Sidebar.Position("[data-md-component=navigation]","[data-md-component=header]"))),document.querySelector("[data-md-component=toc]")&&new f.default.Event.MatchMedia("(min-width: 960px)",new f.default.Event.Listener(window,["scroll","resize","orientationchange"],new f.default.Sidebar.Position("[data-md-component=toc]","[data-md-component=header]"))),new f.default.Event.MatchMedia("(min-width: 960px)",new f.default.Event.Listener(window,"scroll",new f.default.Nav.Blur("[data-md-component=toc] [href]")));var n=document.querySelectorAll("[data-md-component=collapsible]");Array.prototype.forEach.call(n,function(e){new f.default.Event.MatchMedia("(min-width: 1220px)",new f.default.Event.Listener(e.previousElementSibling,"click",new f.default.Nav.Collapse(e)))}),new f.default.Event.MatchMedia("(max-width: 1219px)",new f.default.Event.Listener("[data-md-component=navigation] [data-md-toggle]","change",new f.default.Nav.Scrolling("[data-md-component=navigation] nav"))),document.querySelector("[data-md-component=search]")&&(new f.default.Event.MatchMedia("(max-width: 959px)",new f.default.Event.Listener("[data-md-toggle=search]","change",new f.default.Search.Lock("[data-md-toggle=search]"))),new f.default.Event.Listener("[data-md-component=query]",["focus","keyup","change"],new f.default.Search.Result("[data-md-component=result]",function(){return fetch(t.url.base+"/"+(t.version<"0.17"?"mkdocs":"search")+"/search_index.json",{credentials:"same-origin"}).then(function(e){return e.json()}).then(function(e){return e.docs.map(function(e){return e.location=t.url.base+"/"+e.location,e})})})).listen(),new f.default.Event.Listener("[data-md-component=reset]","click",function(){setTimeout(function(){var e=document.querySelector("[data-md-component=query]");if(!(e instanceof HTMLInputElement))throw new ReferenceError;e.focus()},10)}).listen(),new f.default.Event.Listener("[data-md-toggle=search]","change",function(e){setTimeout(function(e){if(!(e instanceof HTMLInputElement))throw new ReferenceError;if(e.checked){var t=document.querySelector("[data-md-component=query]");if(!(t instanceof HTMLInputElement))throw new ReferenceError;t.focus()}},400,e.target)}).listen(),new f.default.Event.MatchMedia("(min-width: 960px)",new f.default.Event.Listener("[data-md-component=query]","focus",function(){var e=document.querySelector("[data-md-toggle=search]");if(!(e instanceof HTMLInputElement))throw new ReferenceError;e.checked||(e.checked=!0,e.dispatchEvent(new CustomEvent("change")))})),new f.default.Event.Listener(window,"keydown",function(e){var t=document.querySelector("[data-md-toggle=search]");if(!(t instanceof HTMLInputElement))throw new ReferenceError;var n=document.querySelector("[data-md-component=query]");if(!(n instanceof HTMLInputElement))throw new ReferenceError;if(!e.metaKey&&!e.ctrlKey)if(t.checked){if(13===e.keyCode){if(n===document.activeElement){e.preventDefault();var r=document.querySelector("[data-md-component=search] [href][data-md-state=active]");r instanceof HTMLLinkElement&&(window.location=r.getAttribute("href"),t.checked=!1,t.dispatchEvent(new CustomEvent("change")),n.blur())}}else if(9===e.keyCode||27===e.keyCode)t.checked=!1,t.dispatchEvent(new CustomEvent("change")),n.blur();else if(-1!==[8,37,39].indexOf(e.keyCode))n!==document.activeElement&&n.focus();else if(-1!==[38,40].indexOf(e.keyCode)){var i=e.keyCode,o=Array.prototype.slice.call(document.querySelectorAll("[data-md-component=query], [data-md-component=search] [href]")),a=o.find(function(e){if(!(e instanceof HTMLElement))throw new ReferenceError;return"active"===e.dataset.mdState});a&&(a.dataset.mdState="");var s=Math.max(0,(o.indexOf(a)+o.length+(38===i?-1:1))%o.length);return o[s]&&(o[s].dataset.mdState="active",o[s].focus()),e.preventDefault(),e.stopPropagation(),!1}}else document.activeElement&&!document.activeElement.form&&(70!==e.keyCode&&83!==e.keyCode||(n.focus(),e.preventDefault()))}).listen(),new f.default.Event.Listener(window,"keypress",function(){var e=document.querySelector("[data-md-toggle=search]");if(!(e instanceof HTMLInputElement))throw new ReferenceError;if(e.checked){var t=document.querySelector("[data-md-component=query]");if(!(t instanceof HTMLInputElement))throw new ReferenceError;t!==document.activeElement&&t.focus()}}).listen()),new f.default.Event.Listener(document.body,"keydown",function(e){if(9===e.keyCode){var t=document.querySelectorAll("[data-md-component=navigation] .md-nav__link[for]:not([tabindex])");Array.prototype.forEach.call(t,function(e){e.offsetHeight&&(e.tabIndex=0)})}}).listen(),new f.default.Event.Listener(document.body,"mousedown",function(){var e=document.querySelectorAll("[data-md-component=navigation] .md-nav__link[tabindex]");Array.prototype.forEach.call(e,function(e){e.removeAttribute("tabIndex")})}).listen(),document.body.addEventListener("click",function(){"tabbing"===document.body.dataset.mdState&&(document.body.dataset.mdState="")}),new f.default.Event.MatchMedia("(max-width: 959px)",new f.default.Event.Listener("[data-md-component=navigation] [href^='#']","click",function(){var e=document.querySelector("[data-md-toggle=drawer]");if(!(e instanceof HTMLInputElement))throw new ReferenceError;e.checked&&(e.checked=!1,e.dispatchEvent(new CustomEvent("change")))})),function(){var e=document.querySelector("[data-md-source]");if(!e)return a.default.resolve([]);if(!(e instanceof HTMLAnchorElement))throw new ReferenceError;switch(e.dataset.mdSource){case"github":return new f.default.Source.Adapter.GitHub(e).fetch();default:return a.default.resolve([])}}().then(function(e){var t=document.querySelectorAll("[data-md-source]");Array.prototype.forEach.call(t,function(t){new f.default.Source.Repository(t).initialize(e)})})}t.__esModule=!0,t.app=void 0,n(7),n(8),n(9),n(10),n(11),n(12),n(13);var o=n(14),a=r(o),s=n(19),c=r(s),u=n(20),l=r(u),d=n(21),f=r(d);window.Promise=window.Promise||a.default;var h=function(e){var t=document.getElementsByName("lang:"+e)[0];if(!(t instanceof HTMLMetaElement))throw new ReferenceError;return t.content},p={initialize:i};t.app=p}).call(t,n(0))},function(e,t,n){e.exports=n.p+"assets/images/icons/bitbucket.1b09e088.svg"},function(e,t,n){e.exports=n.p+"assets/images/icons/github.f0b8504a.svg"},function(e,t,n){e.exports=n.p+"assets/images/icons/gitlab.6dd19c00.svg"},function(e,t){},function(e,t){},function(e,t){!function(){if("undefined"!=typeof window)try{var e=new window.CustomEvent("test",{cancelable:!0});if(e.preventDefault(),!0!==e.defaultPrevented)throw new Error("Could not prevent default")}catch(e){var t=function(e,t){var n,r;return t=t||{bubbles:!1,cancelable:!1,detail:void 0},n=document.createEvent("CustomEvent"),n.initCustomEvent(e,t.bubbles,t.cancelable,t.detail),r=n.preventDefault,n.preventDefault=function(){r.call(this);try{Object.defineProperty(this,"defaultPrevented",{get:function(){return!0}})}catch(e){this.defaultPrevented=!0}},n};t.prototype=window.Event.prototype,window.CustomEvent=t}}()},function(e,t,n){window.fetch||(window.fetch=n(2).default||n(2))},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),function(e){function r(){}function i(e,t){return function(){e.apply(t,arguments)}}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],d(e,this)}function a(e,t){for(;3===e._state;)e=e._value;if(0===e._state)return void e._deferreds.push(t);e._handled=!0,o._immediateFn(function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null===n)return void(1===e._state?s:c)(t.promise,e._value);var r;try{r=n(e._value)}catch(e){return void c(t.promise,e)}s(t.promise,r)})}function s(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof o)return e._state=3,e._value=t,void u(e);if("function"==typeof n)return void d(i(n,t),e)}e._state=1,e._value=t,u(e)}catch(t){c(e,t)}}function c(e,t){e._state=2,e._value=t,u(e)}function u(e){2===e._state&&0===e._deferreds.length&&o._immediateFn(function(){e._handled||o._unhandledRejectionFn(e._value)});for(var t=0,n=e._deferreds.length;t=0&&(e._idleTimeoutId=setTimeout(function(){e._onTimeout&&e._onTimeout()},t))},n(16),t.setImmediate="undefined"!=typeof self&&self.setImmediate||void 0!==e&&e.setImmediate||this&&this.setImmediate,t.clearImmediate="undefined"!=typeof self&&self.clearImmediate||void 0!==e&&e.clearImmediate||this&&this.clearImmediate}).call(t,n(1))},function(e,t,n){(function(e,t){!function(e,n){"use strict";function r(e){"function"!=typeof e&&(e=new Function(""+e));for(var t=new Array(arguments.length-1),n=0;n1)for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=e.action,this.container=e.container,this.emitter=e.emitter,this.target=e.target,this.text=e.text,this.trigger=e.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var e=this,t="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return e.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[t?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,r.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,r.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var e=void 0;try{e=document.execCommand(this.action)}catch(t){e=!1}this.handleResult(e)}},{key:"handleResult",value:function(e){this.emitter.emit(e?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=e,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(e){if(void 0!==e){if(!e||"object"!==(void 0===e?"undefined":i(e))||1!==e.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&e.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(e.hasAttribute("readonly")||e.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=e}},get:function(){return this._target}}]),e}();e.exports=a})},function(e,t,n){function r(e,t,n){if(!e&&!t&&!n)throw new Error("Missing required arguments");if(!s.string(t))throw new TypeError("Second argument must be a String");if(!s.fn(n))throw new TypeError("Third argument must be a Function");if(s.node(e))return i(e,t,n);if(s.nodeList(e))return o(e,t,n);if(s.string(e))return a(e,t,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function i(e,t,n){return e.addEventListener(t,n),{destroy:function(){e.removeEventListener(t,n)}}}function o(e,t,n){return Array.prototype.forEach.call(e,function(e){e.addEventListener(t,n)}),{destroy:function(){Array.prototype.forEach.call(e,function(e){e.removeEventListener(t,n)})}}}function a(e,t,n){return c(document.body,e,t,n)}var s=n(6),c=n(5);e.exports=r},function(e,t){function n(){}n.prototype={on:function(e,t,n){var r=this.e||(this.e={});return(r[e]||(r[e]=[])).push({fn:t,ctx:n}),this},once:function(e,t,n){function r(){i.off(e,r),t.apply(n,arguments)}var i=this;return r._=t,this.on(e,r,n)},emit:function(e){var t=[].slice.call(arguments,1),n=((this.e||(this.e={}))[e]||[]).slice(),r=0,i=n.length;for(r;r0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof e.action?e.action:this.defaultAction,this.target="function"==typeof e.target?e.target:this.defaultTarget,this.text="function"==typeof e.text?e.text:this.defaultText,this.container="object"===f(e.container)?e.container:document.body}},{key:"listenClick",value:function(e){var t=this;this.listener=(0,d.default)(e,"click",function(e){return t.onClick(e)})}},{key:"onClick",value:function(e){var t=e.delegateTarget||e.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new u.default({action:this.action(t),target:this.target(t),text:this.text(t),container:this.container,trigger:t,emitter:this})}},{key:"defaultAction",value:function(e){return c("action",e)}},{key:"defaultTarget",value:function(e){var t=c("target",e);if(t)return document.querySelector(t)}},{key:"defaultText",value:function(e){return c("text",e)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],t="string"==typeof e?[e]:e,n=!!document.queryCommandSupported;return t.forEach(function(e){n=n&&!!document.queryCommandSupported(e)}),n}}]),t}(l.default);e.exports=p})},function(e,t){function n(e,t){for(;e&&e.nodeType!==r;){if("function"==typeof e.matches&&e.matches(t))return e;e=e.parentNode}}var r=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var i=Element.prototype;i.matches=i.matchesSelector||i.mozMatchesSelector||i.msMatchesSelector||i.oMatchesSelector||i.webkitMatchesSelector}e.exports=n},function(e,t,n){function r(e,t,n,r,i){var a=o.apply(this,arguments);return e.addEventListener(n,a,i),{destroy:function(){e.removeEventListener(n,a,i)}}}function i(e,t,n,i,o){return"function"==typeof e.addEventListener?r.apply(null,arguments):"function"==typeof n?r.bind(null,document).apply(null,arguments):("string"==typeof e&&(e=document.querySelectorAll(e)),Array.prototype.map.call(e,function(e){return r(e,t,n,i,o)}))}function o(e,t,n,r){return function(n){n.delegateTarget=a(n.target,t),n.delegateTarget&&r.call(e,n)}}var a=n(4);e.exports=i},function(e,t){t.node=function(e){return void 0!==e&&e instanceof HTMLElement&&1===e.nodeType},t.nodeList=function(e){var n=Object.prototype.toString.call(e);return void 0!==e&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in e&&(0===e.length||t.node(e[0]))},t.string=function(e){return"string"==typeof e||e instanceof String},t.fn=function(e){return"[object Function]"===Object.prototype.toString.call(e)}},function(e,t){function n(e){var t;if("SELECT"===e.nodeName)e.focus(),t=e.value;else if("INPUT"===e.nodeName||"TEXTAREA"===e.nodeName){var n=e.hasAttribute("readonly");n||e.setAttribute("readonly",""),e.select(),e.setSelectionRange(0,e.value.length),n||e.removeAttribute("readonly"),t=e.value}else{e.hasAttribute("contenteditable")&&e.focus();var r=window.getSelection(),i=document.createRange();i.selectNodeContents(e),r.removeAllRanges(),r.addRange(i),t=r.toString()}return t}e.exports=n}])})},function(e,t,n){var r;!function(){"use strict";function i(e,t){var n;if(t=t||{},this.trackingClick=!1,this.trackingClickStart=0,this.targetElement=null,this.touchStartX=0,this.touchStartY=0,this.lastTouchIdentifier=0,this.touchBoundary=t.touchBoundary||10,this.layer=e,this.tapDelay=t.tapDelay||200,this.tapTimeout=t.tapTimeout||700,!i.notNeeded(e)){for(var r=["onMouse","onClick","onTouchStart","onTouchMove","onTouchEnd","onTouchCancel"],o=this,s=0,c=r.length;s=0,a=navigator.userAgent.indexOf("Android")>0&&!o,s=/iP(ad|hone|od)/.test(navigator.userAgent)&&!o,c=s&&/OS 4_\d(_\d)?/.test(navigator.userAgent),u=s&&/OS [6-7]_\d/.test(navigator.userAgent),l=navigator.userAgent.indexOf("BB10")>0;i.prototype.needsClick=function(e){switch(e.nodeName.toLowerCase()){case"button":case"select":case"textarea":if(e.disabled)return!0;break;case"input":if(s&&"file"===e.type||e.disabled)return!0;break;case"label":case"iframe":case"video":return!0}return/\bneedsclick\b/.test(e.className)},i.prototype.needsFocus=function(e){switch(e.nodeName.toLowerCase()){case"textarea":return!0;case"select":return!a;case"input":switch(e.type){case"button":case"checkbox":case"file":case"image":case"radio":case"submit":return!1}return!e.disabled&&!e.readOnly;default:return/\bneedsfocus\b/.test(e.className)}},i.prototype.sendClick=function(e,t){var n,r;document.activeElement&&document.activeElement!==e&&document.activeElement.blur(),r=t.changedTouches[0],n=document.createEvent("MouseEvents"),n.initMouseEvent(this.determineEventType(e),!0,!0,window,1,r.screenX,r.screenY,r.clientX,r.clientY,!1,!1,!1,!1,0,null),n.forwardedTouchEvent=!0,e.dispatchEvent(n)},i.prototype.determineEventType=function(e){return a&&"select"===e.tagName.toLowerCase()?"mousedown":"click"},i.prototype.focus=function(e){var t;s&&e.setSelectionRange&&0!==e.type.indexOf("date")&&"time"!==e.type&&"month"!==e.type?(t=e.value.length,e.setSelectionRange(t,t)):e.focus()},i.prototype.updateScrollParent=function(e){var t,n;if(!(t=e.fastClickScrollParent)||!t.contains(e)){n=e;do{if(n.scrollHeight>n.offsetHeight){t=n,e.fastClickScrollParent=n;break}n=n.parentElement}while(n)}t&&(t.fastClickLastScrollTop=t.scrollTop)},i.prototype.getTargetElementFromEventTarget=function(e){return e.nodeType===Node.TEXT_NODE?e.parentNode:e},i.prototype.onTouchStart=function(e){var t,n,r;if(e.targetTouches.length>1)return!0;if(t=this.getTargetElementFromEventTarget(e.target),n=e.targetTouches[0],s){if(r=window.getSelection(),r.rangeCount&&!r.isCollapsed)return!0;if(!c){if(n.identifier&&n.identifier===this.lastTouchIdentifier)return e.preventDefault(),!1;this.lastTouchIdentifier=n.identifier,this.updateScrollParent(t)}}return this.trackingClick=!0,this.trackingClickStart=e.timeStamp,this.targetElement=t,this.touchStartX=n.pageX,this.touchStartY=n.pageY,e.timeStamp-this.lastClickTimen||Math.abs(t.pageY-this.touchStartY)>n},i.prototype.onTouchMove=function(e){return!this.trackingClick||((this.targetElement!==this.getTargetElementFromEventTarget(e.target)||this.touchHasMoved(e))&&(this.trackingClick=!1,this.targetElement=null),!0)},i.prototype.findControl=function(e){return void 0!==e.control?e.control:e.htmlFor?document.getElementById(e.htmlFor):e.querySelector("button, input:not([type=hidden]), keygen, meter, output, progress, select, textarea")},i.prototype.onTouchEnd=function(e){var t,n,r,i,o,l=this.targetElement;if(!this.trackingClick)return!0;if(e.timeStamp-this.lastClickTimethis.tapTimeout)return!0;if(this.cancelNextClick=!1,this.lastClickTime=e.timeStamp,n=this.trackingClickStart,this.trackingClick=!1,this.trackingClickStart=0,u&&(o=e.changedTouches[0],l=document.elementFromPoint(o.pageX-window.pageXOffset,o.pageY-window.pageYOffset)||l,l.fastClickScrollParent=this.targetElement.fastClickScrollParent),"label"===(r=l.tagName.toLowerCase())){if(t=this.findControl(l)){if(this.focus(l),a)return!1;l=t}}else if(this.needsFocus(l))return e.timeStamp-n>100||s&&window.top!==window&&"input"===r?(this.targetElement=null,!1):(this.focus(l),this.sendClick(l,e),s&&"select"===r||(this.targetElement=null,e.preventDefault()),!1);return!(!s||c||!(i=l.fastClickScrollParent)||i.fastClickLastScrollTop===i.scrollTop)||(this.needsClick(l)||(e.preventDefault(),this.sendClick(l,e)),!1)},i.prototype.onTouchCancel=function(){this.trackingClick=!1,this.targetElement=null},i.prototype.onMouse=function(e){return!this.targetElement||(!!e.forwardedTouchEvent||(!e.cancelable||(!(!this.needsClick(this.targetElement)||this.cancelNextClick)||(e.stopImmediatePropagation?e.stopImmediatePropagation():e.propagationStopped=!0,e.stopPropagation(),e.preventDefault(),!1))))},i.prototype.onClick=function(e){var t;return this.trackingClick?(this.targetElement=null,this.trackingClick=!1,!0):"submit"===e.target.type&&0===e.detail||(t=this.onMouse(e),t||(this.targetElement=null),t)},i.prototype.destroy=function(){var e=this.layer;a&&(e.removeEventListener("mouseover",this.onMouse,!0),e.removeEventListener("mousedown",this.onMouse,!0),e.removeEventListener("mouseup",this.onMouse,!0)),e.removeEventListener("click",this.onClick,!0),e.removeEventListener("touchstart",this.onTouchStart,!1),e.removeEventListener("touchmove",this.onTouchMove,!1),e.removeEventListener("touchend",this.onTouchEnd,!1),e.removeEventListener("touchcancel",this.onTouchCancel,!1)},i.notNeeded=function(e){var t,n,r;if(void 0===window.ontouchstart)return!0;if(n=+(/Chrome\/([0-9]+)/.exec(navigator.userAgent)||[,0])[1]){if(!a)return!0;if(t=document.querySelector("meta[name=viewport]")){if(-1!==t.content.indexOf("user-scalable=no"))return!0;if(n>31&&document.documentElement.scrollWidth<=window.outerWidth)return!0}}if(l&&(r=navigator.userAgent.match(/Version\/([0-9]*)\.([0-9]*)/),r[1]>=10&&r[2]>=3&&(t=document.querySelector("meta[name=viewport]")))){if(-1!==t.content.indexOf("user-scalable=no"))return!0;if(document.documentElement.scrollWidth<=window.outerWidth)return!0}return"none"===e.style.msTouchAction||"manipulation"===e.style.touchAction||(!!(+(/Firefox\/([0-9]+)/.exec(navigator.userAgent)||[,0])[1]>=27&&(t=document.querySelector("meta[name=viewport]"))&&(-1!==t.content.indexOf("user-scalable=no")||document.documentElement.scrollWidth<=window.outerWidth))||("none"===e.style.touchAction||"manipulation"===e.style.touchAction))},i.attach=function(e,t){return new i(e,t)},void 0!==(r=function(){return i}.call(t,n,t,e))&&(e.exports=r)}()},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(22),o=r(i),a=n(24),s=r(a),c=n(27),u=r(c),l=n(31),d=r(l),f=n(37),h=r(f),p=n(39),m=r(p),v=n(45),y=r(v);t.default={Event:o.default,Header:s.default,Nav:u.default,Search:d.default,Sidebar:h.default,Source:m.default,Tabs:y.default}},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(3),o=r(i),a=n(23),s=r(a);t.default={Listener:o.default,MatchMedia:s.default}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=n(3),o=(function(e){e&&e.__esModule}(i),function e(t,n){r(this,e),this.handler_=function(e){e.matches?n.listen():n.unlisten()};var i=window.matchMedia(t);i.addListener(this.handler_),this.handler_(i)});t.default=o},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(25),o=r(i),a=n(26),s=r(a);t.default={Shadow:o.default,Title:s.default}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t,n){r(this,e);var i="string"==typeof t?document.querySelector(t):t;if(!(i instanceof HTMLElement&&i.parentNode instanceof HTMLElement))throw new ReferenceError;if(this.el_=i.parentNode,!((i="string"==typeof n?document.querySelector(n):n)instanceof HTMLElement))throw new ReferenceError;this.header_=i,this.height_=0,this.active_=!1}return e.prototype.setup=function(){for(var e=this.el_;e=e.previousElementSibling;){if(!(e instanceof HTMLElement))throw new ReferenceError;this.height_+=e.offsetHeight}this.update()},e.prototype.update=function(e){if(!e||"resize"!==e.type&&"orientationchange"!==e.type){var t=window.pageYOffset>=this.height_;t!==this.active_&&(this.header_.dataset.mdState=(this.active_=t)?"shadow":"")}else this.height_=0,this.setup()},e.prototype.reset=function(){this.header_.dataset.mdState="",this.height_=0,this.active_=!1},e}();t.default=i},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t,n){r(this,e);var i="string"==typeof t?document.querySelector(t):t;if(!(i instanceof HTMLElement))throw new ReferenceError;if(this.el_=i,!((i="string"==typeof n?document.querySelector(n):n)instanceof HTMLHeadingElement))throw new ReferenceError;this.header_=i,this.active_=!1}return e.prototype.setup=function(){var e=this;Array.prototype.forEach.call(this.el_.children,function(t){t.style.width=e.el_.offsetWidth-20+"px"})},e.prototype.update=function(e){var t=this,n=window.pageYOffset>=this.header_.offsetTop;n!==this.active_&&(this.el_.dataset.mdState=(this.active_=n)?"active":""),"resize"!==e.type&&"orientationchange"!==e.type||Array.prototype.forEach.call(this.el_.children,function(e){e.style.width=t.el_.offsetWidth-20+"px"})},e.prototype.reset=function(){this.el_.dataset.mdState="",this.el_.style.width="",this.active_=!1},e}();t.default=i},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(28),o=r(i),a=n(29),s=r(a),c=n(30),u=r(c);t.default={Blur:o.default,Collapse:s.default,Scrolling:u.default}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t){r(this,e),this.els_="string"==typeof t?document.querySelectorAll(t):t,this.index_=0,this.offset_=window.pageYOffset,this.dir_=!1,this.anchors_=[].reduce.call(this.els_,function(e,t){return e.concat(document.getElementById(t.hash.substring(1))||[])},[])}return e.prototype.setup=function(){this.update()},e.prototype.update=function(){var e=window.pageYOffset,t=this.offset_-e<0;if(this.dir_!==t&&(this.index_=this.index_=t?0:this.els_.length-1),0!==this.anchors_.length){if(this.offset_<=e)for(var n=this.index_+1;n0&&(this.els_[n-1].dataset.mdState="blur"),this.index_=n;else for(var r=this.index_;r>=0;r--){if(!(this.anchors_[r].offsetTop-80>e)){this.index_=r;break}r>0&&(this.els_[r-1].dataset.mdState="")}this.offset_=e,this.dir_=t}},e.prototype.reset=function(){Array.prototype.forEach.call(this.els_,function(e){e.dataset.mdState=""}),this.index_=0,this.offset_=window.pageYOffset},e}();t.default=i},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t){r(this,e);var n="string"==typeof t?document.querySelector(t):t;if(!(n instanceof HTMLElement))throw new ReferenceError;this.el_=n}return e.prototype.setup=function(){var e=this.el_.getBoundingClientRect().height;this.el_.style.display=e?"block":"none",this.el_.style.overflow=e?"visible":"hidden"},e.prototype.update=function(){var e=this,t=this.el_.getBoundingClientRect().height;if(this.el_.style.display="block",this.el_.style.overflow="",t)this.el_.style.maxHeight=t+"px",requestAnimationFrame(function(){e.el_.setAttribute("data-md-state","animate"),e.el_.style.maxHeight="0px"});else{this.el_.setAttribute("data-md-state","expand"),this.el_.style.maxHeight="";var n=this.el_.getBoundingClientRect().height;this.el_.removeAttribute("data-md-state"),this.el_.style.maxHeight="0px",requestAnimationFrame(function(){e.el_.setAttribute("data-md-state","animate"),e.el_.style.maxHeight=n+"px"})}var r=function e(n){var r=n.target;if(!(r instanceof HTMLElement))throw new ReferenceError;r.removeAttribute("data-md-state"),r.style.maxHeight="",r.style.display=t?"none":"block",r.style.overflow=t?"hidden":"visible",r.removeEventListener("transitionend",e)};this.el_.addEventListener("transitionend",r,!1)},e.prototype.reset=function(){this.el_.dataset.mdState="",this.el_.style.maxHeight="",this.el_.style.display="",this.el_.style.overflow=""},e}();t.default=i},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t){r(this,e);var n="string"==typeof t?document.querySelector(t):t;if(!(n instanceof HTMLElement))throw new ReferenceError;this.el_=n}return e.prototype.setup=function(){this.el_.children[this.el_.children.length-1].style.webkitOverflowScrolling="touch";var e=this.el_.querySelectorAll("[data-md-toggle]");Array.prototype.forEach.call(e,function(e){if(!(e instanceof HTMLInputElement))throw new ReferenceError;if(e.checked){var t=e.nextElementSibling;if(!(t instanceof HTMLElement))throw new ReferenceError;for(;"NAV"!==t.tagName&&t.nextElementSibling;)t=t.nextElementSibling;if(!(e.parentNode instanceof HTMLElement&&e.parentNode.parentNode instanceof HTMLElement))throw new ReferenceError;var n=e.parentNode.parentNode,r=t.children[t.children.length-1];n.style.webkitOverflowScrolling="",r.style.webkitOverflowScrolling="touch"}})},e.prototype.update=function(e){var t=e.target;if(!(t instanceof HTMLElement))throw new ReferenceError;var n=t.nextElementSibling;if(!(n instanceof HTMLElement))throw new ReferenceError;for(;"NAV"!==n.tagName&&n.nextElementSibling;)n=n.nextElementSibling;if(!(t.parentNode instanceof HTMLElement&&t.parentNode.parentNode instanceof HTMLElement))throw new ReferenceError;var r=t.parentNode.parentNode,i=n.children[n.children.length-1];if(r.style.webkitOverflowScrolling="",i.style.webkitOverflowScrolling="",!t.checked){var o=function e(){n instanceof HTMLElement&&(r.style.webkitOverflowScrolling="touch",n.removeEventListener("transitionend",e))};n.addEventListener("transitionend",o,!1)}if(t.checked){var a=function e(){n instanceof HTMLElement&&(i.style.webkitOverflowScrolling="touch",n.removeEventListener("transitionend",e))};n.addEventListener("transitionend",a,!1)}},e.prototype.reset=function(){this.el_.children[1].style.webkitOverflowScrolling="";var e=this.el_.querySelectorAll("[data-md-toggle]");Array.prototype.forEach.call(e,function(e){if(!(e instanceof HTMLInputElement))throw new ReferenceError;if(e.checked){var t=e.nextElementSibling;if(!(t instanceof HTMLElement))throw new ReferenceError;for(;"NAV"!==t.tagName&&t.nextElementSibling;)t=t.nextElementSibling;if(!(e.parentNode instanceof HTMLElement&&e.parentNode.parentNode instanceof HTMLElement))throw new ReferenceError;var n=e.parentNode.parentNode,r=t.children[t.children.length-1];n.style.webkitOverflowScrolling="",r.style.webkitOverflowScrolling=""}})},e}();t.default=i},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(32),o=r(i),a=n(33),s=r(a);t.default={Lock:o.default,Result:s.default}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t){r(this,e);var n="string"==typeof t?document.querySelector(t):t;if(!(n instanceof HTMLInputElement))throw new ReferenceError;if(this.el_=n,!document.body)throw new ReferenceError;this.lock_=document.body}return e.prototype.setup=function(){this.update()},e.prototype.update=function(){var e=this;this.el_.checked?(this.offset_=window.pageYOffset,setTimeout(function(){window.scrollTo(0,0),e.el_.checked&&(e.lock_.dataset.mdState="lock")},400)):(this.lock_.dataset.mdState="",setTimeout(function(){void 0!==e.offset_&&window.scrollTo(0,e.offset_)},100))},e.prototype.reset=function(){"lock"===this.lock_.dataset.mdState&&window.scrollTo(0,this.offset_),this.lock_.dataset.mdState=""},e}();t.default=i},function(e,t,n){"use strict";(function(e){function r(e){return e&&e.__esModule?e:{default:e}}function i(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var o=n(34),a=r(o),s=n(35),c=r(s),u=function(e,t){var n=t;if(e.length>n){for(;" "!==e[n]&&--n>0;);return e.substring(0,n)+"..."}return e},l=function(e){var t=document.getElementsByName("lang:"+e)[0];if(!(t instanceof HTMLMetaElement))throw new ReferenceError;return t.content},d=function(){function t(e,n){i(this,t);var r="string"==typeof e?document.querySelector(e):e;if(!(r instanceof HTMLElement))throw new ReferenceError;this.el_=r;var o=Array.prototype.slice.call(this.el_.children),a=o[0],s=o[1];this.data_=n,this.meta_=a,this.list_=s,this.message_={placeholder:this.meta_.textContent,none:l("search.result.none"),one:l("search.result.one"),other:l("search.result.other")};var u=l("search.tokenizer");u.length&&(c.default.tokenizer.separator=u),this.lang_=l("search.language").split(",").filter(Boolean).map(function(e){return e.trim()})}return t.prototype.update=function(t){var n=this;if("focus"!==t.type||this.index_){if("focus"===t.type||"keyup"===t.type){var r=t.target;if(!(r instanceof HTMLInputElement))throw new ReferenceError;if(!this.index_||r.value===this.value_)return;for(;this.list_.firstChild;)this.list_.removeChild(this.list_.firstChild);if(this.value_=r.value,0===this.value_.length)return void(this.meta_.textContent=this.message_.placeholder);var i=this.index_.query(function(e){n.value_.toLowerCase().split(" ").filter(Boolean).forEach(function(t){e.term(t,{wildcard:c.default.Query.wildcard.TRAILING})})}).reduce(function(e,t){var r=n.docs_.get(t.ref);if(r.parent){var i=r.parent.location;e.set(i,(e.get(i)||[]).concat(t))}else{var o=r.location;e.set(o,e.get(o)||[])}return e},new Map),o=(0,a.default)(this.value_.trim()).replace(new RegExp(c.default.tokenizer.separator,"img"),"|"),s=new RegExp("(^|"+c.default.tokenizer.separator+")("+o+")","img"),d=function(e,t,n){return t+""+n+""};this.stack_=[],i.forEach(function(t,r){var i,o=n.docs_.get(r),a=e.createElement("li",{class:"md-search-result__item"},e.createElement("a",{href:o.location,title:o.title,class:"md-search-result__link",tabindex:"-1"},e.createElement("article",{class:"md-search-result__article md-search-result__article--document"},e.createElement("h1",{class:"md-search-result__title"},{__html:o.title.replace(s,d)}),o.text.length?e.createElement("p",{class:"md-search-result__teaser"},{__html:o.text.replace(s,d)}):{}))),c=t.map(function(t){return function(){var r=n.docs_.get(t.ref);a.appendChild(e.createElement("a",{href:r.location,title:r.title,class:"md-search-result__link","data-md-rel":"anchor",tabindex:"-1"},e.createElement("article",{class:"md-search-result__article"},e.createElement("h1",{class:"md-search-result__title"},{__html:r.title.replace(s,d)}),r.text.length?e.createElement("p",{class:"md-search-result__teaser"},{__html:u(r.text.replace(s,d),400)}):{})))}});(i=n.stack_).push.apply(i,[function(){return n.list_.appendChild(a)}].concat(c))});var f=this.el_.parentNode;if(!(f instanceof HTMLElement))throw new ReferenceError;for(;this.stack_.length&&f.offsetHeight>=f.scrollHeight-16;)this.stack_.shift()();var h=this.list_.querySelectorAll("[data-md-rel=anchor]");switch(Array.prototype.forEach.call(h,function(e){["click","keydown"].forEach(function(t){e.addEventListener(t,function(n){if("keydown"!==t||13===n.keyCode){var r=document.querySelector("[data-md-toggle=search]");if(!(r instanceof HTMLInputElement))throw new ReferenceError;r.checked&&(r.checked=!1,r.dispatchEvent(new CustomEvent("change"))),n.preventDefault(),setTimeout(function(){document.location.href=e.href},100)}})})}),i.size){case 0:this.meta_.textContent=this.message_.none;break;case 1:this.meta_.textContent=this.message_.one;break;default:this.meta_.textContent=this.message_.other.replace("#",i.size)}}}else{var p=function(e){n.docs_=e.reduce(function(e,t){var n=t.location.split("#"),r=n[0];return n[1]&&(t.parent=e.get(r),t.parent&&!t.parent.done&&(t.parent.title=t.title,t.parent.text=t.text,t.parent.done=!0)),t.text=t.text.replace(/\n/g," ").replace(/\s+/g," ").replace(/\s+([,.:;!?])/g,function(e,t){return t}),t.parent&&t.parent.title===t.title||e.set(t.location,t),e},new Map);var t=n.docs_,r=n.lang_;n.stack_=[],n.index_=(0,c.default)(function(){var e,n=this,i={"search.pipeline.trimmer":c.default.trimmer,"search.pipeline.stopwords":c.default.stopWordFilter},o=Object.keys(i).reduce(function(e,t){return l(t).match(/^false$/i)||e.push(i[t]),e},[]);this.pipeline.reset(),o&&(e=this.pipeline).add.apply(e,o),1===r.length&&"en"!==r[0]&&c.default[r[0]]?this.use(c.default[r[0]]):r.length>1&&this.use(c.default.multiLanguage.apply(c.default,r)),this.field("title",{boost:10}),this.field("text"),this.ref("location"),t.forEach(function(e){return n.add(e)})});var i=n.el_.parentNode;if(!(i instanceof HTMLElement))throw new ReferenceError;i.addEventListener("scroll",function(){for(;n.stack_.length&&i.scrollTop+i.offsetHeight>=i.scrollHeight-16;)n.stack_.splice(0,10).forEach(function(e){return e()})})};setTimeout(function(){return"function"==typeof n.data_?n.data_().then(p):p(n.data_)},250)}},t}();t.default=d}).call(t,n(0))},function(e,t,n){"use strict";var r=/[|\\{}()[\]^$+*?.]/g;e.exports=function(e){if("string"!=typeof e)throw new TypeError("Expected a string");return e.replace(r,"\\$&")}},function(e,t,n){(function(t){e.exports=t.lunr=n(36)}).call(t,n(1))},function(e,t,n){var r,i;!function(){var o=function(e){var t=new o.Builder;return t.pipeline.add(o.trimmer,o.stopWordFilter,o.stemmer),t.searchPipeline.add(o.stemmer),e.call(t,t),t.build()};o.version="2.1.5",o.utils={},o.utils.warn=function(e){return function(t){e.console&&console.warn&&console.warn(t)}}(this),o.utils.asString=function(e){return void 0===e||null===e?"":e.toString()},o.FieldRef=function(e,t,n){this.docRef=e,this.fieldName=t,this._stringValue=n},o.FieldRef.joiner="/",o.FieldRef.fromString=function(e){var t=e.indexOf(o.FieldRef.joiner);if(-1===t)throw"malformed field ref string";var n=e.slice(0,t),r=e.slice(t+1);return new o.FieldRef(r,n,e)},o.FieldRef.prototype.toString=function(){return void 0==this._stringValue&&(this._stringValue=this.fieldName+o.FieldRef.joiner+this.docRef),this._stringValue},o.idf=function(e,t){var n=0;for(var r in e)"_index"!=r&&(n+=Object.keys(e[r]).length);var i=(t-n+.5)/(n+.5);return Math.log(1+Math.abs(i))},o.Token=function(e,t){this.str=e||"",this.metadata=t||{}},o.Token.prototype.toString=function(){return this.str},o.Token.prototype.update=function(e){return this.str=e(this.str,this.metadata),this},o.Token.prototype.clone=function(e){return e=e||function(e){return e},new o.Token(e(this.str,this.metadata),this.metadata)},o.tokenizer=function(e){if(null==e||void 0==e)return[];if(Array.isArray(e))return e.map(function(e){return new o.Token(o.utils.asString(e).toLowerCase())});for(var t=e.toString().trim().toLowerCase(),n=t.length,r=[],i=0,a=0;i<=n;i++){var s=t.charAt(i),c=i-a;(s.match(o.tokenizer.separator)||i==n)&&(c>0&&r.push(new o.Token(t.slice(a,i),{position:[a,c],index:r.length})),a=i+1)}return r},o.tokenizer.separator=/[\s\-]+/,o.Pipeline=function(){this._stack=[]},o.Pipeline.registeredFunctions=Object.create(null),o.Pipeline.registerFunction=function(e,t){t in this.registeredFunctions&&o.utils.warn("Overwriting existing registered function: "+t),e.label=t,o.Pipeline.registeredFunctions[e.label]=e},o.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||o.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},o.Pipeline.load=function(e){var t=new o.Pipeline;return e.forEach(function(e){var n=o.Pipeline.registeredFunctions[e];if(!n)throw new Error("Cannot load unregistered function: "+e);t.add(n)}),t},o.Pipeline.prototype.add=function(){Array.prototype.slice.call(arguments).forEach(function(e){o.Pipeline.warnIfFunctionNotRegistered(e),this._stack.push(e)},this)},o.Pipeline.prototype.after=function(e,t){o.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw new Error("Cannot find existingFn");n+=1,this._stack.splice(n,0,t)},o.Pipeline.prototype.before=function(e,t){o.Pipeline.warnIfFunctionNotRegistered(t);var n=this._stack.indexOf(e);if(-1==n)throw new Error("Cannot find existingFn");this._stack.splice(n,0,t)},o.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);-1!=t&&this._stack.splice(t,1)},o.Pipeline.prototype.run=function(e){for(var t=this._stack.length,n=0;n1&&(oe&&(n=i),o!=e);)r=n-t,i=t+Math.floor(r/2),o=this.elements[2*i];return o==e?2*i:o>e?2*i:os?u+=2:a==s&&(t+=n[c+1]*r[u+1],c+=2,u+=2);return t},o.Vector.prototype.similarity=function(e){return this.dot(e)/(this.magnitude()*e.magnitude())},o.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),t=1,n=0;t0){var a,s=i.str.charAt(0);s in i.node.edges?a=i.node.edges[s]:(a=new o.TokenSet,i.node.edges[s]=a),1==i.str.length?a.final=!0:r.push({node:a,editsRemaining:i.editsRemaining,str:i.str.slice(1)})}if(i.editsRemaining>0&&i.str.length>1){var c,s=i.str.charAt(1);s in i.node.edges?c=i.node.edges[s]:(c=new o.TokenSet,i.node.edges[s]=c),i.str.length<=2?c.final=!0:r.push({node:c,editsRemaining:i.editsRemaining-1,str:i.str.slice(2)})}if(i.editsRemaining>0&&1==i.str.length&&(i.node.final=!0),i.editsRemaining>0&&i.str.length>=1){if("*"in i.node.edges)var u=i.node.edges["*"];else{var u=new o.TokenSet;i.node.edges["*"]=u}1==i.str.length?u.final=!0:r.push({node:u,editsRemaining:i.editsRemaining-1,str:i.str.slice(1)})}if(i.editsRemaining>0){if("*"in i.node.edges)var l=i.node.edges["*"];else{var l=new o.TokenSet;i.node.edges["*"]=l}0==i.str.length?l.final=!0:r.push({node:l,editsRemaining:i.editsRemaining-1,str:i.str})}if(i.editsRemaining>0&&i.str.length>1){var d,f=i.str.charAt(0),h=i.str.charAt(1);h in i.node.edges?d=i.node.edges[h]:(d=new o.TokenSet,i.node.edges[h]=d),1==i.str.length?d.final=!0:r.push({node:d,editsRemaining:i.editsRemaining-1,str:f+i.str.slice(2)})}}return n},o.TokenSet.fromString=function(e){for(var t=new o.TokenSet,n=t,r=!1,i=0,a=e.length;i=e;t--){var n=this.uncheckedNodes[t],r=n.child.toString();r in this.minimizedNodes?n.parent.edges[n.char]=this.minimizedNodes[r]:(n.child._str=r,this.minimizedNodes[r]=n.child),this.uncheckedNodes.pop()}},o.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},o.Index.prototype.search=function(e){return this.query(function(t){new o.QueryParser(e,t).parse()})},o.Index.prototype.query=function(e){var t=new o.Query(this.fields),n=Object.create(null),r=Object.create(null),i=Object.create(null);e.call(t,t);for(var a=0;a1?1:e},o.Builder.prototype.k1=function(e){this._k1=e},o.Builder.prototype.add=function(e){var t=e[this._ref];this.documentCount+=1;for(var n=0;n=this.length)return o.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},o.QueryLexer.prototype.width=function(){return this.pos-this.start},o.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},o.QueryLexer.prototype.backup=function(){this.pos-=1},o.QueryLexer.prototype.acceptDigitRun=function(){var e,t;do{e=this.next(),t=e.charCodeAt(0)}while(t>47&&t<58);e!=o.QueryLexer.EOS&&this.backup()},o.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(o.QueryLexer.TERM)),e.ignore(),e.more())return o.QueryLexer.lexText},o.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(o.QueryLexer.EDIT_DISTANCE),o.QueryLexer.lexText},o.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(o.QueryLexer.BOOST),o.QueryLexer.lexText},o.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(o.QueryLexer.TERM)},o.QueryLexer.termSeparator=o.tokenizer.separator,o.QueryLexer.lexText=function(e){for(;;){var t=e.next();if(t==o.QueryLexer.EOS)return o.QueryLexer.lexEOS;if(92!=t.charCodeAt(0)){if(":"==t)return o.QueryLexer.lexField;if("~"==t)return e.backup(),e.width()>0&&e.emit(o.QueryLexer.TERM),o.QueryLexer.lexEditDistance;if("^"==t)return e.backup(),e.width()>0&&e.emit(o.QueryLexer.TERM),o.QueryLexer.lexBoost;if(t.match(o.QueryLexer.termSeparator))return o.QueryLexer.lexTerm}else e.escapeCharacter()}},o.QueryParser=function(e,t){this.lexer=new o.QueryLexer(e),this.query=t,this.currentClause={},this.lexemeIdx=0},o.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=o.QueryParser.parseFieldOrTerm;e;)e=e(this);return this.query},o.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},o.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},o.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},o.QueryParser.parseFieldOrTerm=function(e){var t=e.peekLexeme();if(void 0!=t)switch(t.type){case o.QueryLexer.FIELD:return o.QueryParser.parseField;case o.QueryLexer.TERM:return o.QueryParser.parseTerm;default:var n="expected either a field or a term, found "+t.type;throw t.str.length>=1&&(n+=" with value '"+t.str+"'"),new o.QueryParseError(n,t.start,t.end)}},o.QueryParser.parseField=function(e){var t=e.consumeLexeme();if(void 0!=t){if(-1==e.query.allFields.indexOf(t.str)){var n=e.query.allFields.map(function(e){return"'"+e+"'"}).join(", "),r="unrecognised field '"+t.str+"', possible fields: "+n;throw new o.QueryParseError(r,t.start,t.end)}e.currentClause.fields=[t.str];var i=e.peekLexeme();if(void 0==i){var r="expecting term, found nothing";throw new o.QueryParseError(r,t.start,t.end)}switch(i.type){case o.QueryLexer.TERM:return o.QueryParser.parseTerm;default:var r="expecting term, found '"+i.type+"'";throw new o.QueryParseError(r,i.start,i.end)}}},o.QueryParser.parseTerm=function(e){var t=e.consumeLexeme();if(void 0!=t){e.currentClause.term=t.str.toLowerCase(),-1!=t.str.indexOf("*")&&(e.currentClause.usePipeline=!1);var n=e.peekLexeme();if(void 0==n)return void e.nextClause();switch(n.type){case o.QueryLexer.TERM:return e.nextClause(),o.QueryParser.parseTerm;case o.QueryLexer.FIELD:return e.nextClause(),o.QueryParser.parseField;case o.QueryLexer.EDIT_DISTANCE:return o.QueryParser.parseEditDistance;case o.QueryLexer.BOOST:return o.QueryParser.parseBoost;default:var r="Unexpected lexeme type '"+n.type+"'";throw new o.QueryParseError(r,n.start,n.end)}}},o.QueryParser.parseEditDistance=function(e){var t=e.consumeLexeme();if(void 0!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="edit distance must be numeric";throw new o.QueryParseError(r,t.start,t.end)}e.currentClause.editDistance=n;var i=e.peekLexeme();if(void 0==i)return void e.nextClause();switch(i.type){case o.QueryLexer.TERM:return e.nextClause(),o.QueryParser.parseTerm;case o.QueryLexer.FIELD:return e.nextClause(),o.QueryParser.parseField;case o.QueryLexer.EDIT_DISTANCE:return o.QueryParser.parseEditDistance;case o.QueryLexer.BOOST:return o.QueryParser.parseBoost;default:var r="Unexpected lexeme type '"+i.type+"'";throw new o.QueryParseError(r,i.start,i.end)}}},o.QueryParser.parseBoost=function(e){var t=e.consumeLexeme();if(void 0!=t){var n=parseInt(t.str,10);if(isNaN(n)){var r="boost must be numeric";throw new o.QueryParseError(r,t.start,t.end)}e.currentClause.boost=n;var i=e.peekLexeme();if(void 0==i)return void e.nextClause();switch(i.type){case o.QueryLexer.TERM:return e.nextClause(),o.QueryParser.parseTerm;case o.QueryLexer.FIELD:return e.nextClause(),o.QueryParser.parseField;case o.QueryLexer.EDIT_DISTANCE:return o.QueryParser.parseEditDistance;case o.QueryLexer.BOOST:return o.QueryParser.parseBoost;default:var r="Unexpected lexeme type '"+i.type+"'";throw new o.QueryParseError(r,i.start,i.end)}}},function(o,a){r=a,void 0!==(i="function"==typeof r?r.call(t,n,t,e):r)&&(e.exports=i)}(0,function(){return o})}()},function(e,t,n){"use strict";t.__esModule=!0;var r=n(38),i=function(e){return e&&e.__esModule?e:{default:e}}(r);t.default={Position:i.default}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=function(){function e(t,n){r(this,e);var i="string"==typeof t?document.querySelector(t):t;if(!(i instanceof HTMLElement&&i.parentNode instanceof HTMLElement))throw new ReferenceError;if(this.el_=i,this.parent_=i.parentNode,!((i="string"==typeof n?document.querySelector(n):n)instanceof HTMLElement))throw new ReferenceError;this.header_=i,this.height_=0,this.pad_="fixed"===window.getComputedStyle(this.header_).position}return e.prototype.setup=function(){var e=Array.prototype.reduce.call(this.parent_.children,function(e,t){return Math.max(e,t.offsetTop)},0);this.offset_=e-(this.pad_?this.header_.offsetHeight:0),this.update()},e.prototype.update=function(e){var t=window.pageYOffset,n=window.innerHeight;e&&"resize"===e.type&&this.setup();var r={top:this.pad_?this.header_.offsetHeight:0,bottom:this.parent_.offsetTop+this.parent_.offsetHeight},i=n-r.top-Math.max(0,this.offset_-t)-Math.max(0,t+n-r.bottom);i!==this.height_&&(this.el_.style.height=(this.height_=i)+"px"),t>=this.offset_?"lock"!==this.el_.dataset.mdState&&(this.el_.dataset.mdState="lock"):"lock"===this.el_.dataset.mdState&&(this.el_.dataset.mdState="")},e.prototype.reset=function(){this.el_.dataset.mdState="",this.el_.style.height="",this.height_=0},e}();t.default=i},function(e,t,n){"use strict";function r(e){return e&&e.__esModule?e:{default:e}}t.__esModule=!0;var i=n(40),o=r(i),a=n(44),s=r(a);t.default={Adapter:o.default,Repository:s.default}},function(e,t,n){"use strict";t.__esModule=!0;var r=n(41),i=function(e){return e&&e.__esModule?e:{default:e}}(r);t.default={GitHub:i.default}},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function i(e,t){if(!e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!t||"object"!=typeof t&&"function"!=typeof t?e:t}function o(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}t.__esModule=!0;var a=n(42),s=function(e){return e&&e.__esModule?e:{default:e}}(a),c=function(e){function t(n){r(this,t);var o=i(this,e.call(this,n)),a=/^.+github\.com\/([^\/]+)\/?([^\/]+)?.*$/.exec(o.base_);if(a&&3===a.length){var s=a[1],c=a[2];o.base_="https://api.github.com/users/"+s+"/repos",o.name_=c}return o}return o(t,e),t.prototype.fetch_=function(){var e=this;return function t(){var n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;return fetch(e.base_+"?per_page=30&page="+n).then(function(e){return e.json()}).then(function(r){if(!(r instanceof Array))throw new TypeError;if(e.name_){var i=r.find(function(t){return t.name===e.name_});return i||30!==r.length?i?[e.format_(i.stargazers_count)+" Stars",e.format_(i.forks_count)+" Forks"]:[]:t(n+1)}return[r.length+" Repositories"]})}()},t}(s.default);t.default=c},function(e,t,n){"use strict";function r(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}t.__esModule=!0;var i=n(43),o=function(e){return e&&e.__esModule?e:{default:e}}(i),a=function(){function e(t){r(this,e);var n="string"==typeof t?document.querySelector(t):t;if(!(n instanceof HTMLAnchorElement))throw new ReferenceError;this.el_=n,this.base_=this.el_.href,this.salt_=this.hash_(this.base_)}return e.prototype.fetch=function(){var e=this;return new Promise(function(t){var n=o.default.getJSON(e.salt_+".cache-source");void 0!==n?t(n):e.fetch_().then(function(n){o.default.set(e.salt_+".cache-source",n,{expires:1/96}),t(n)})})},e.prototype.fetch_=function(){throw new Error("fetch_(): Not implemented")},e.prototype.format_=function(e){return e>1e4?(e/1e3).toFixed(0)+"k":e>1e3?(e/1e3).toFixed(1)+"k":""+e},e.prototype.hash_=function(e){var t=0;if(0===e.length)return t;for(var n=0,r=e.length;n1){if(o=e({path:"/"},r.defaults,o),"number"==typeof o.expires){var s=new Date;s.setMilliseconds(s.getMilliseconds()+864e5*o.expires),o.expires=s}o.expires=o.expires?o.expires.toUTCString():"";try{a=JSON.stringify(i),/^[\{\[]/.test(a)&&(i=a)}catch(e){}i=n.write?n.write(i,t):encodeURIComponent(String(i)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g,decodeURIComponent),t=encodeURIComponent(String(t)),t=t.replace(/%(23|24|26|2B|5E|60|7C)/g,decodeURIComponent),t=t.replace(/[\(\)]/g,escape);var c="";for(var u in o)o[u]&&(c+="; "+u,!0!==o[u]&&(c+="="+o[u]));return document.cookie=t+"="+i+c}t||(a={});for(var l=document.cookie?document.cookie.split("; "):[],d=/(%[0-9A-Z]{2})+/g,f=0;f=this.el_.children[0].offsetTop+-43;e!==this.active_&&(this.el_.dataset.mdState=(this.active_=e)?"hidden":"")},e.prototype.reset=function(){this.el_.dataset.mdState="",this.active_=!1},e}();t.default=i}])); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.da.js b/v1/assets/javascripts/lunr/lunr.da.js new file mode 100644 index 000000000..3b07b2c19 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.da.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r,i,n;e.da=function(){this.pipeline.reset(),this.pipeline.add(e.da.trimmer,e.da.stopWordFilter,e.da.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.da.stemmer))},e.da.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.da.trimmer=e.trimmerSupport.generateTrimmer(e.da.wordCharacters),e.Pipeline.registerFunction(e.da.trimmer,"trimmer-da"),e.da.stemmer=(r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){var e,n,t,s=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],o=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],a=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],u=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],c=new i;function l(){var e,r=c.limit-c.cursor;c.cursor>=n&&(e=c.limit_backward,c.limit_backward=n,c.ket=c.cursor,c.find_among_b(o,4)?(c.bra=c.cursor,c.limit_backward=e,c.cursor=c.limit-r,c.cursor>c.limit_backward&&(c.cursor--,c.bra=c.cursor,c.slice_del())):c.limit_backward=e)}this.setCurrent=function(e){c.setCurrent(e)},this.getCurrent=function(){return c.getCurrent()},this.stem=function(){var r,i=c.cursor;return function(){var r,i=c.cursor+3;if(n=c.limit,0<=i&&i<=c.limit){for(e=i;;){if(r=c.cursor,c.in_grouping(d,97,248)){c.cursor=r;break}if(c.cursor=r,r>=c.limit)return;c.cursor++}for(;!c.out_grouping(d,97,248);){if(c.cursor>=c.limit)return;c.cursor++}(n=c.cursor)=n&&(r=c.limit_backward,c.limit_backward=n,c.ket=c.cursor,e=c.find_among_b(s,32),c.limit_backward=r,e))switch(c.bra=c.cursor,e){case 1:c.slice_del();break;case 2:c.in_grouping_b(u,97,229)&&c.slice_del()}}(),c.cursor=c.limit,l(),c.cursor=c.limit,function(){var e,r,i,t=c.limit-c.cursor;if(c.ket=c.cursor,c.eq_s_b(2,"st")&&(c.bra=c.cursor,c.eq_s_b(2,"ig")&&c.slice_del()),c.cursor=c.limit-t,c.cursor>=n&&(r=c.limit_backward,c.limit_backward=n,c.ket=c.cursor,e=c.find_among_b(a,5),c.limit_backward=r,e))switch(c.bra=c.cursor,e){case 1:c.slice_del(),i=c.limit-c.cursor,l(),c.cursor=c.limit-i;break;case 2:c.slice_from("løs")}}(),c.cursor=c.limit,c.cursor>=n&&(r=c.limit_backward,c.limit_backward=n,c.ket=c.cursor,c.out_grouping_b(d,97,248)?(c.bra=c.cursor,t=c.slice_to(t),c.limit_backward=r,c.eq_v_b(t)&&c.slice_del()):c.limit_backward=r),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.de.js b/v1/assets/javascripts/lunr/lunr.de.js new file mode 100644 index 000000000..ebd78f281 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.de.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r,n,i;e.de=function(){this.pipeline.reset(),this.pipeline.add(e.de.trimmer,e.de.stopWordFilter,e.de.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.de.stemmer))},e.de.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.de.trimmer=e.trimmerSupport.generateTrimmer(e.de.wordCharacters),e.Pipeline.registerFunction(e.de.trimmer,"trimmer-de"),e.de.stemmer=(r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){var e,i,s,t=[new r("",-1,6),new r("U",0,2),new r("Y",0,1),new r("ä",0,3),new r("ö",0,4),new r("ü",0,5)],o=[new r("e",-1,2),new r("em",-1,1),new r("en",-1,2),new r("ern",-1,1),new r("er",-1,1),new r("s",-1,3),new r("es",5,2)],c=[new r("en",-1,1),new r("er",-1,1),new r("st",-1,2),new r("est",2,1)],u=[new r("ig",-1,1),new r("lich",-1,1)],a=[new r("end",-1,1),new r("ig",-1,2),new r("ung",-1,1),new r("lich",-1,3),new r("isch",-1,2),new r("ik",-1,2),new r("heit",-1,3),new r("keit",-1,4)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32,8],l=[117,30,5],m=[117,30,4],h=new n;function w(e,r,n){return!(!h.eq_s(1,e)||(h.ket=h.cursor,!h.in_grouping(d,97,252)))&&(h.slice_from(r),h.cursor=n,!0)}function f(){for(;!h.in_grouping(d,97,252);){if(h.cursor>=h.limit)return!0;h.cursor++}for(;!h.out_grouping(d,97,252);){if(h.cursor>=h.limit)return!0;h.cursor++}return!1}function b(){return s<=h.cursor}function _(){return i<=h.cursor}this.setCurrent=function(e){h.setCurrent(e)},this.getCurrent=function(){return h.getCurrent()},this.stem=function(){var r=h.cursor;return function(){for(var e,r,n,i,s=h.cursor;;)if(e=h.cursor,h.bra=e,h.eq_s(1,"ß"))h.ket=h.cursor,h.slice_from("ss");else{if(e>=h.limit)break;h.cursor=e+1}for(h.cursor=s;;)for(r=h.cursor;;){if(n=h.cursor,h.in_grouping(d,97,252)){if(i=h.cursor,h.bra=i,w("u","U",n))break;if(h.cursor=i,w("y","Y",n))break}if(n>=h.limit)return void(h.cursor=r);h.cursor=n+1}}(),h.cursor=r,function(){s=h.limit,i=s;var r=h.cursor+3;0<=r&&r<=h.limit&&(e=r,f()||((s=h.cursor)=h.limit)return;h.cursor++}}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}),e.Pipeline.registerFunction(e.de.stemmer,"stemmer-de"),e.de.stopWordFilter=e.generateStopWordFilter("aber alle allem allen aller alles als also am an ander andere anderem anderen anderer anderes anderm andern anderr anders auch auf aus bei bin bis bist da damit dann das dasselbe dazu daß dein deine deinem deinen deiner deines dem demselben den denn denselben der derer derselbe derselben des desselben dessen dich die dies diese dieselbe dieselben diesem diesen dieser dieses dir doch dort du durch ein eine einem einen einer eines einig einige einigem einigen einiger einiges einmal er es etwas euch euer eure eurem euren eurer eures für gegen gewesen hab habe haben hat hatte hatten hier hin hinter ich ihm ihn ihnen ihr ihre ihrem ihren ihrer ihres im in indem ins ist jede jedem jeden jeder jedes jene jenem jenen jener jenes jetzt kann kein keine keinem keinen keiner keines können könnte machen man manche manchem manchen mancher manches mein meine meinem meinen meiner meines mich mir mit muss musste nach nicht nichts noch nun nur ob oder ohne sehr sein seine seinem seinen seiner seines selbst sich sie sind so solche solchem solchen solcher solches soll sollte sondern sonst um und uns unse unsem unsen unser unses unter viel vom von vor war waren warst was weg weil weiter welche welchem welchen welcher welches wenn werde werden wie wieder will wir wird wirst wo wollen wollte während würde würden zu zum zur zwar zwischen über".split(" ")),e.Pipeline.registerFunction(e.de.stopWordFilter,"stopWordFilter-de")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.du.js b/v1/assets/javascripts/lunr/lunr.du.js new file mode 100644 index 000000000..375c0e763 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.du.js @@ -0,0 +1 @@ +!function(r,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():e()(r.lunr)}(this,function(){return function(r){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var e,i,n;r.du=function(){this.pipeline.reset(),this.pipeline.add(r.du.trimmer,r.du.stopWordFilter,r.du.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(r.du.stemmer))},r.du.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",r.du.trimmer=r.trimmerSupport.generateTrimmer(r.du.wordCharacters),r.Pipeline.registerFunction(r.du.trimmer,"trimmer-du"),r.du.stemmer=(e=r.stemmerSupport.Among,i=r.stemmerSupport.SnowballProgram,n=new function(){var r,n,o,t=[new e("",-1,6),new e("á",0,1),new e("ä",0,1),new e("é",0,2),new e("ë",0,2),new e("í",0,3),new e("ï",0,3),new e("ó",0,4),new e("ö",0,4),new e("ú",0,5),new e("ü",0,5)],s=[new e("",-1,3),new e("I",0,2),new e("Y",0,1)],u=[new e("dd",-1,-1),new e("kk",-1,-1),new e("tt",-1,-1)],c=[new e("ene",-1,2),new e("se",-1,3),new e("en",-1,2),new e("heden",2,1),new e("s",-1,3)],a=[new e("end",-1,1),new e("ig",-1,2),new e("ing",-1,1),new e("lijk",-1,3),new e("baar",-1,4),new e("bar",-1,5)],l=[new e("aa",-1,-1),new e("ee",-1,-1),new e("oo",-1,-1),new e("uu",-1,-1)],m=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],d=[1,0,0,17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],f=[17,67,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],_=new i;function w(r){return _.cursor=r,r>=_.limit||(_.cursor++,!1)}function b(){for(;!_.in_grouping(m,97,232);){if(_.cursor>=_.limit)return!0;_.cursor++}for(;!_.out_grouping(m,97,232);){if(_.cursor>=_.limit)return!0;_.cursor++}return!1}function p(){return n<=_.cursor}function g(){return r<=_.cursor}function h(){var r=_.limit-_.cursor;_.find_among_b(u,3)&&(_.cursor=_.limit-r,_.ket=_.cursor,_.cursor>_.limit_backward&&(_.cursor--,_.bra=_.cursor,_.slice_del()))}function k(){var r;o=!1,_.ket=_.cursor,_.eq_s_b(1,"e")&&(_.bra=_.cursor,p()&&(r=_.limit-_.cursor,_.out_grouping_b(m,97,232)&&(_.cursor=_.limit-r,_.slice_del(),o=!0,h())))}function v(){var r;p()&&(r=_.limit-_.cursor,_.out_grouping_b(m,97,232)&&(_.cursor=_.limit-r,_.eq_s_b(3,"gem")||(_.cursor=_.limit-r,_.slice_del(),h())))}this.setCurrent=function(r){_.setCurrent(r)},this.getCurrent=function(){return _.getCurrent()},this.stem=function(){var e=_.cursor;return function(){for(var r,e,i,n=_.cursor;;){if(_.bra=_.cursor,r=_.find_among(t,11))switch(_.ket=_.cursor,r){case 1:_.slice_from("a");continue;case 2:_.slice_from("e");continue;case 3:_.slice_from("i");continue;case 4:_.slice_from("o");continue;case 5:_.slice_from("u");continue;case 6:if(_.cursor>=_.limit)break;_.cursor++;continue}break}for(_.cursor=n,_.bra=n,_.eq_s(1,"y")?(_.ket=_.cursor,_.slice_from("Y")):_.cursor=n;;)if(e=_.cursor,_.in_grouping(m,97,232)){if(i=_.cursor,_.bra=i,_.eq_s(1,"i"))_.ket=_.cursor,_.in_grouping(m,97,232)&&(_.slice_from("I"),_.cursor=e);else if(_.cursor=i,_.eq_s(1,"y"))_.ket=_.cursor,_.slice_from("Y"),_.cursor=e;else if(w(e))break}else if(w(e))break}(),_.cursor=e,n=_.limit,r=n,b()||((n=_.cursor)<3&&(n=3),b()||(r=_.cursor)),_.limit_backward=e,_.cursor=_.limit,function(){var r,e,i,n,t,s,u=_.limit-_.cursor;if(_.ket=_.cursor,r=_.find_among_b(c,5))switch(_.bra=_.cursor,r){case 1:p()&&_.slice_from("heid");break;case 2:v();break;case 3:p()&&_.out_grouping_b(f,97,232)&&_.slice_del()}if(_.cursor=_.limit-u,k(),_.cursor=_.limit-u,_.ket=_.cursor,_.eq_s_b(4,"heid")&&(_.bra=_.cursor,g()&&(e=_.limit-_.cursor,_.eq_s_b(1,"c")||(_.cursor=_.limit-e,_.slice_del(),_.ket=_.cursor,_.eq_s_b(2,"en")&&(_.bra=_.cursor,v())))),_.cursor=_.limit-u,_.ket=_.cursor,r=_.find_among_b(a,6))switch(_.bra=_.cursor,r){case 1:if(g()){if(_.slice_del(),i=_.limit-_.cursor,_.ket=_.cursor,_.eq_s_b(2,"ig")&&(_.bra=_.cursor,g()&&(n=_.limit-_.cursor,!_.eq_s_b(1,"e")))){_.cursor=_.limit-n,_.slice_del();break}_.cursor=_.limit-i,h()}break;case 2:g()&&(t=_.limit-_.cursor,_.eq_s_b(1,"e")||(_.cursor=_.limit-t,_.slice_del()));break;case 3:g()&&(_.slice_del(),k());break;case 4:g()&&_.slice_del();break;case 5:g()&&o&&_.slice_del()}_.cursor=_.limit-u,_.out_grouping_b(d,73,232)&&(s=_.limit-_.cursor,_.find_among_b(l,4)&&_.out_grouping_b(m,97,232)&&(_.cursor=_.limit-s,_.ket=_.cursor,_.cursor>_.limit_backward&&(_.cursor--,_.bra=_.cursor,_.slice_del())))}(),_.cursor=_.limit_backward,function(){for(var r;;)if(_.bra=_.cursor,r=_.find_among(s,3))switch(_.ket=_.cursor,r){case 1:_.slice_from("y");break;case 2:_.slice_from("i");break;case 3:if(_.cursor>=_.limit)return;_.cursor++}}(),!0}},function(r){return"function"==typeof r.update?r.update(function(r){return n.setCurrent(r),n.stem(),n.getCurrent()}):(n.setCurrent(r),n.stem(),n.getCurrent())}),r.Pipeline.registerFunction(r.du.stemmer,"stemmer-du"),r.du.stopWordFilter=r.generateStopWordFilter(" aan al alles als altijd andere ben bij daar dan dat de der deze die dit doch doen door dus een eens en er ge geen geweest haar had heb hebben heeft hem het hier hij hoe hun iemand iets ik in is ja je kan kon kunnen maar me meer men met mij mijn moet na naar niet niets nog nu of om omdat onder ons ook op over reeds te tegen toch toen tot u uit uw van veel voor want waren was wat werd wezen wie wil worden wordt zal ze zelf zich zij zijn zo zonder zou".split(" ")),r.Pipeline.registerFunction(r.du.stopWordFilter,"stopWordFilter-du")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.es.js b/v1/assets/javascripts/lunr/lunr.es.js new file mode 100644 index 000000000..4cb634f0a --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.es.js @@ -0,0 +1 @@ +!function(e,s){"function"==typeof define&&define.amd?define(s):"object"==typeof exports?module.exports=s():s()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var s,r,n;e.es=function(){this.pipeline.reset(),this.pipeline.add(e.es.trimmer,e.es.stopWordFilter,e.es.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.es.stemmer))},e.es.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.es.trimmer=e.trimmerSupport.generateTrimmer(e.es.wordCharacters),e.Pipeline.registerFunction(e.es.trimmer,"trimmer-es"),e.es.stemmer=(s=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){var e,n,i,a=[new s("",-1,6),new s("á",0,1),new s("é",0,2),new s("í",0,3),new s("ó",0,4),new s("ú",0,5)],t=[new s("la",-1,-1),new s("sela",0,-1),new s("le",-1,-1),new s("me",-1,-1),new s("se",-1,-1),new s("lo",-1,-1),new s("selo",5,-1),new s("las",-1,-1),new s("selas",7,-1),new s("les",-1,-1),new s("los",-1,-1),new s("selos",10,-1),new s("nos",-1,-1)],o=[new s("ando",-1,6),new s("iendo",-1,6),new s("yendo",-1,7),new s("ándo",-1,2),new s("iéndo",-1,1),new s("ar",-1,6),new s("er",-1,6),new s("ir",-1,6),new s("ár",-1,3),new s("ér",-1,4),new s("ír",-1,5)],u=[new s("ic",-1,-1),new s("ad",-1,-1),new s("os",-1,-1),new s("iv",-1,1)],w=[new s("able",-1,1),new s("ible",-1,1),new s("ante",-1,1)],c=[new s("ic",-1,1),new s("abil",-1,1),new s("iv",-1,1)],m=[new s("ica",-1,1),new s("ancia",-1,2),new s("encia",-1,5),new s("adora",-1,2),new s("osa",-1,1),new s("ista",-1,1),new s("iva",-1,9),new s("anza",-1,1),new s("logía",-1,3),new s("idad",-1,8),new s("able",-1,1),new s("ible",-1,1),new s("ante",-1,2),new s("mente",-1,7),new s("amente",13,6),new s("ación",-1,2),new s("ución",-1,4),new s("ico",-1,1),new s("ismo",-1,1),new s("oso",-1,1),new s("amiento",-1,1),new s("imiento",-1,1),new s("ivo",-1,9),new s("ador",-1,2),new s("icas",-1,1),new s("ancias",-1,2),new s("encias",-1,5),new s("adoras",-1,2),new s("osas",-1,1),new s("istas",-1,1),new s("ivas",-1,9),new s("anzas",-1,1),new s("logías",-1,3),new s("idades",-1,8),new s("ables",-1,1),new s("ibles",-1,1),new s("aciones",-1,2),new s("uciones",-1,4),new s("adores",-1,2),new s("antes",-1,2),new s("icos",-1,1),new s("ismos",-1,1),new s("osos",-1,1),new s("amientos",-1,1),new s("imientos",-1,1),new s("ivos",-1,9)],l=[new s("ya",-1,1),new s("ye",-1,1),new s("yan",-1,1),new s("yen",-1,1),new s("yeron",-1,1),new s("yendo",-1,1),new s("yo",-1,1),new s("yas",-1,1),new s("yes",-1,1),new s("yais",-1,1),new s("yamos",-1,1),new s("yó",-1,1)],d=[new s("aba",-1,2),new s("ada",-1,2),new s("ida",-1,2),new s("ara",-1,2),new s("iera",-1,2),new s("ía",-1,2),new s("aría",5,2),new s("ería",5,2),new s("iría",5,2),new s("ad",-1,2),new s("ed",-1,2),new s("id",-1,2),new s("ase",-1,2),new s("iese",-1,2),new s("aste",-1,2),new s("iste",-1,2),new s("an",-1,2),new s("aban",16,2),new s("aran",16,2),new s("ieran",16,2),new s("ían",16,2),new s("arían",20,2),new s("erían",20,2),new s("irían",20,2),new s("en",-1,1),new s("asen",24,2),new s("iesen",24,2),new s("aron",-1,2),new s("ieron",-1,2),new s("arán",-1,2),new s("erán",-1,2),new s("irán",-1,2),new s("ado",-1,2),new s("ido",-1,2),new s("ando",-1,2),new s("iendo",-1,2),new s("ar",-1,2),new s("er",-1,2),new s("ir",-1,2),new s("as",-1,2),new s("abas",39,2),new s("adas",39,2),new s("idas",39,2),new s("aras",39,2),new s("ieras",39,2),new s("ías",39,2),new s("arías",45,2),new s("erías",45,2),new s("irías",45,2),new s("es",-1,1),new s("ases",49,2),new s("ieses",49,2),new s("abais",-1,2),new s("arais",-1,2),new s("ierais",-1,2),new s("íais",-1,2),new s("aríais",55,2),new s("eríais",55,2),new s("iríais",55,2),new s("aseis",-1,2),new s("ieseis",-1,2),new s("asteis",-1,2),new s("isteis",-1,2),new s("áis",-1,2),new s("éis",-1,1),new s("aréis",64,2),new s("eréis",64,2),new s("iréis",64,2),new s("ados",-1,2),new s("idos",-1,2),new s("amos",-1,2),new s("ábamos",70,2),new s("áramos",70,2),new s("iéramos",70,2),new s("íamos",70,2),new s("aríamos",74,2),new s("eríamos",74,2),new s("iríamos",74,2),new s("emos",-1,1),new s("aremos",78,2),new s("eremos",78,2),new s("iremos",78,2),new s("ásemos",78,2),new s("iésemos",78,2),new s("imos",-1,2),new s("arás",-1,2),new s("erás",-1,2),new s("irás",-1,2),new s("ís",-1,2),new s("ará",-1,2),new s("erá",-1,2),new s("irá",-1,2),new s("aré",-1,2),new s("eré",-1,2),new s("iré",-1,2),new s("ió",-1,2)],b=[new s("a",-1,1),new s("e",-1,2),new s("o",-1,1),new s("os",-1,1),new s("á",-1,1),new s("é",-1,2),new s("í",-1,1),new s("ó",-1,1)],f=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,1,17,4,10],_=new r;function h(){if(_.out_grouping(f,97,252)){for(;!_.in_grouping(f,97,252);){if(_.cursor>=_.limit)return!0;_.cursor++}return!1}return!0}function v(){var e,s=_.cursor;if(function(){if(_.in_grouping(f,97,252)){var e=_.cursor;if(h()){if(_.cursor=e,!_.in_grouping(f,97,252))return!0;for(;!_.out_grouping(f,97,252);){if(_.cursor>=_.limit)return!0;_.cursor++}}return!1}return!0}()){if(_.cursor=s,!_.out_grouping(f,97,252))return;if(e=_.cursor,h()){if(_.cursor=e,!_.in_grouping(f,97,252)||_.cursor>=_.limit)return;_.cursor++}}i=_.cursor}function p(){for(;!_.in_grouping(f,97,252);){if(_.cursor>=_.limit)return!1;_.cursor++}for(;!_.out_grouping(f,97,252);){if(_.cursor>=_.limit)return!1;_.cursor++}return!0}function g(){return i<=_.cursor}function k(){return e<=_.cursor}function y(e,s){if(!k())return!0;_.slice_del(),_.ket=_.cursor;var r=_.find_among_b(e,s);return r&&(_.bra=_.cursor,1==r&&k()&&_.slice_del()),!1}function q(e){return!k()||(_.slice_del(),_.ket=_.cursor,_.eq_s_b(2,e)&&(_.bra=_.cursor,k()&&_.slice_del()),!1)}function C(){var e;if(_.ket=_.cursor,e=_.find_among_b(m,46)){switch(_.bra=_.cursor,e){case 1:if(!k())return!1;_.slice_del();break;case 2:if(q("ic"))return!1;break;case 3:if(!k())return!1;_.slice_from("log");break;case 4:if(!k())return!1;_.slice_from("u");break;case 5:if(!k())return!1;_.slice_from("ente");break;case 6:if(!(n<=_.cursor))return!1;_.slice_del(),_.ket=_.cursor,(e=_.find_among_b(u,4))&&(_.bra=_.cursor,k()&&(_.slice_del(),1==e&&(_.ket=_.cursor,_.eq_s_b(2,"at")&&(_.bra=_.cursor,k()&&_.slice_del()))));break;case 7:if(y(w,3))return!1;break;case 8:if(y(c,3))return!1;break;case 9:if(q("at"))return!1}return!0}return!1}this.setCurrent=function(e){_.setCurrent(e)},this.getCurrent=function(){return _.getCurrent()},this.stem=function(){var s,r=_.cursor;return s=_.cursor,i=_.limit,n=i,e=i,v(),_.cursor=s,p()&&(n=_.cursor,p()&&(e=_.cursor)),_.limit_backward=r,_.cursor=_.limit,function(){var e;if(_.ket=_.cursor,_.find_among_b(t,13)&&(_.bra=_.cursor,(e=_.find_among_b(o,11))&&g()))switch(e){case 1:_.bra=_.cursor,_.slice_from("iendo");break;case 2:_.bra=_.cursor,_.slice_from("ando");break;case 3:_.bra=_.cursor,_.slice_from("ar");break;case 4:_.bra=_.cursor,_.slice_from("er");break;case 5:_.bra=_.cursor,_.slice_from("ir");break;case 6:_.slice_del();break;case 7:_.eq_s_b(1,"u")&&_.slice_del()}}(),_.cursor=_.limit,C()||(_.cursor=_.limit,function(){var e,s;if(_.cursor>=i&&(s=_.limit_backward,_.limit_backward=i,_.ket=_.cursor,e=_.find_among_b(l,12),_.limit_backward=s,e)){if(_.bra=_.cursor,1==e){if(!_.eq_s_b(1,"u"))return!1;_.slice_del()}return!0}return!1}()||(_.cursor=_.limit,function(){var e,s,r,n;if(_.cursor>=i&&(s=_.limit_backward,_.limit_backward=i,_.ket=_.cursor,e=_.find_among_b(d,96),_.limit_backward=s,e))switch(_.bra=_.cursor,e){case 1:r=_.limit-_.cursor,_.eq_s_b(1,"u")?(n=_.limit-_.cursor,_.eq_s_b(1,"g")?_.cursor=_.limit-n:_.cursor=_.limit-r):_.cursor=_.limit-r,_.bra=_.cursor;case 2:_.slice_del()}}())),_.cursor=_.limit,function(){var e,s;if(_.ket=_.cursor,e=_.find_among_b(b,8))switch(_.bra=_.cursor,e){case 1:g()&&_.slice_del();break;case 2:g()&&(_.slice_del(),_.ket=_.cursor,_.eq_s_b(1,"u")&&(_.bra=_.cursor,s=_.limit-_.cursor,_.eq_s_b(1,"g")&&(_.cursor=_.limit-s,g()&&_.slice_del())))}}(),_.cursor=_.limit_backward,function(){for(var e;;){if(_.bra=_.cursor,e=_.find_among(a,6))switch(_.ket=_.cursor,e){case 1:_.slice_from("a");continue;case 2:_.slice_from("e");continue;case 3:_.slice_from("i");continue;case 4:_.slice_from("o");continue;case 5:_.slice_from("u");continue;case 6:if(_.cursor>=_.limit)break;_.cursor++;continue}break}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}),e.Pipeline.registerFunction(e.es.stemmer,"stemmer-es"),e.es.stopWordFilter=e.generateStopWordFilter("a al algo algunas algunos ante antes como con contra cual cuando de del desde donde durante e el ella ellas ellos en entre era erais eran eras eres es esa esas ese eso esos esta estaba estabais estaban estabas estad estada estadas estado estados estamos estando estar estaremos estará estarán estarás estaré estaréis estaría estaríais estaríamos estarían estarías estas este estemos esto estos estoy estuve estuviera estuvierais estuvieran estuvieras estuvieron estuviese estuvieseis estuviesen estuvieses estuvimos estuviste estuvisteis estuviéramos estuviésemos estuvo está estábamos estáis están estás esté estéis estén estés fue fuera fuerais fueran fueras fueron fuese fueseis fuesen fueses fui fuimos fuiste fuisteis fuéramos fuésemos ha habida habidas habido habidos habiendo habremos habrá habrán habrás habré habréis habría habríais habríamos habrían habrías habéis había habíais habíamos habían habías han has hasta hay haya hayamos hayan hayas hayáis he hemos hube hubiera hubierais hubieran hubieras hubieron hubiese hubieseis hubiesen hubieses hubimos hubiste hubisteis hubiéramos hubiésemos hubo la las le les lo los me mi mis mucho muchos muy más mí mía mías mío míos nada ni no nos nosotras nosotros nuestra nuestras nuestro nuestros o os otra otras otro otros para pero poco por porque que quien quienes qué se sea seamos sean seas seremos será serán serás seré seréis sería seríais seríamos serían serías seáis sido siendo sin sobre sois somos son soy su sus suya suyas suyo suyos sí también tanto te tendremos tendrá tendrán tendrás tendré tendréis tendría tendríais tendríamos tendrían tendrías tened tenemos tenga tengamos tengan tengas tengo tengáis tenida tenidas tenido tenidos teniendo tenéis tenía teníais teníamos tenían tenías ti tiene tienen tienes todo todos tu tus tuve tuviera tuvierais tuvieran tuvieras tuvieron tuviese tuvieseis tuviesen tuvieses tuvimos tuviste tuvisteis tuviéramos tuviésemos tuvo tuya tuyas tuyo tuyos tú un una uno unos vosotras vosotros vuestra vuestras vuestro vuestros y ya yo él éramos".split(" ")),e.Pipeline.registerFunction(e.es.stopWordFilter,"stopWordFilter-es")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.fi.js b/v1/assets/javascripts/lunr/lunr.fi.js new file mode 100644 index 000000000..0200b1fcb --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.fi.js @@ -0,0 +1 @@ +!function(i,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():e()(i.lunr)}(this,function(){return function(i){if(void 0===i)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===i.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var e,r,n;i.fi=function(){this.pipeline.reset(),this.pipeline.add(i.fi.trimmer,i.fi.stopWordFilter,i.fi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(i.fi.stemmer))},i.fi.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",i.fi.trimmer=i.trimmerSupport.generateTrimmer(i.fi.wordCharacters),i.Pipeline.registerFunction(i.fi.trimmer,"trimmer-fi"),i.fi.stemmer=(e=i.stemmerSupport.Among,r=i.stemmerSupport.SnowballProgram,n=new function(){var i,n,t,s,l=[new e("pa",-1,1),new e("sti",-1,2),new e("kaan",-1,1),new e("han",-1,1),new e("kin",-1,1),new e("hän",-1,1),new e("kään",-1,1),new e("ko",-1,1),new e("pä",-1,1),new e("kö",-1,1)],o=[new e("lla",-1,-1),new e("na",-1,-1),new e("ssa",-1,-1),new e("ta",-1,-1),new e("lta",3,-1),new e("sta",3,-1)],a=[new e("llä",-1,-1),new e("nä",-1,-1),new e("ssä",-1,-1),new e("tä",-1,-1),new e("ltä",3,-1),new e("stä",3,-1)],u=[new e("lle",-1,-1),new e("ine",-1,-1)],c=[new e("nsa",-1,3),new e("mme",-1,3),new e("nne",-1,3),new e("ni",-1,2),new e("si",-1,1),new e("an",-1,4),new e("en",-1,6),new e("än",-1,5),new e("nsä",-1,3)],m=[new e("aa",-1,-1),new e("ee",-1,-1),new e("ii",-1,-1),new e("oo",-1,-1),new e("uu",-1,-1),new e("ää",-1,-1),new e("öö",-1,-1)],w=[new e("a",-1,8),new e("lla",0,-1),new e("na",0,-1),new e("ssa",0,-1),new e("ta",0,-1),new e("lta",4,-1),new e("sta",4,-1),new e("tta",4,9),new e("lle",-1,-1),new e("ine",-1,-1),new e("ksi",-1,-1),new e("n",-1,7),new e("han",11,1),new e("den",11,-1,C),new e("seen",11,-1,v),new e("hen",11,2),new e("tten",11,-1,C),new e("hin",11,3),new e("siin",11,-1,C),new e("hon",11,4),new e("hän",11,5),new e("hön",11,6),new e("ä",-1,8),new e("llä",22,-1),new e("nä",22,-1),new e("ssä",22,-1),new e("tä",22,-1),new e("ltä",26,-1),new e("stä",26,-1),new e("ttä",26,9)],_=[new e("eja",-1,-1),new e("mma",-1,1),new e("imma",1,-1),new e("mpa",-1,1),new e("impa",3,-1),new e("mmi",-1,1),new e("immi",5,-1),new e("mpi",-1,1),new e("impi",7,-1),new e("ejä",-1,-1),new e("mmä",-1,1),new e("immä",10,-1),new e("mpä",-1,1),new e("impä",12,-1)],k=[new e("i",-1,-1),new e("j",-1,-1)],b=[new e("mma",-1,1),new e("imma",0,-1)],d=[17,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8],f=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],h=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],p=[17,97,24,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],g=new r;function j(){for(var i;i=g.cursor,!g.in_grouping(f,97,246);){if(g.cursor=i,i>=g.limit)return!0;g.cursor++}for(g.cursor=i;!g.out_grouping(f,97,246);){if(g.cursor>=g.limit)return!0;g.cursor++}return!1}function q(){var i,e;if(g.cursor>=s)if(e=g.limit_backward,g.limit_backward=s,g.ket=g.cursor,i=g.find_among_b(l,10)){switch(g.bra=g.cursor,g.limit_backward=e,i){case 1:if(!g.in_grouping_b(p,97,246))return;break;case 2:if(!(t<=g.cursor))return}g.slice_del()}else g.limit_backward=e}function v(){return g.find_among_b(m,7)}function C(){return g.eq_s_b(1,"i")&&g.in_grouping_b(h,97,246)}this.setCurrent=function(i){g.setCurrent(i)},this.getCurrent=function(){return g.getCurrent()},this.stem=function(){var e,r=g.cursor;return s=g.limit,t=s,j()||(s=g.cursor,j()||(t=g.cursor)),i=!1,g.limit_backward=r,g.cursor=g.limit,q(),g.cursor=g.limit,function(){var i,e,r;if(g.cursor>=s)if(e=g.limit_backward,g.limit_backward=s,g.ket=g.cursor,i=g.find_among_b(c,9))switch(g.bra=g.cursor,g.limit_backward=e,i){case 1:r=g.limit-g.cursor,g.eq_s_b(1,"k")||(g.cursor=g.limit-r,g.slice_del());break;case 2:g.slice_del(),g.ket=g.cursor,g.eq_s_b(3,"kse")&&(g.bra=g.cursor,g.slice_from("ksi"));break;case 3:g.slice_del();break;case 4:g.find_among_b(o,6)&&g.slice_del();break;case 5:g.find_among_b(a,6)&&g.slice_del();break;case 6:g.find_among_b(u,2)&&g.slice_del()}else g.limit_backward=e}(),g.cursor=g.limit,function(){var e,r,n;if(g.cursor>=s)if(r=g.limit_backward,g.limit_backward=s,g.ket=g.cursor,e=g.find_among_b(w,30)){switch(g.bra=g.cursor,g.limit_backward=r,e){case 1:if(!g.eq_s_b(1,"a"))return;break;case 2:case 9:if(!g.eq_s_b(1,"e"))return;break;case 3:if(!g.eq_s_b(1,"i"))return;break;case 4:if(!g.eq_s_b(1,"o"))return;break;case 5:if(!g.eq_s_b(1,"ä"))return;break;case 6:if(!g.eq_s_b(1,"ö"))return;break;case 7:if(n=g.limit-g.cursor,!v()&&(g.cursor=g.limit-n,!g.eq_s_b(2,"ie"))){g.cursor=g.limit-n;break}if(g.cursor=g.limit-n,g.cursor<=g.limit_backward){g.cursor=g.limit-n;break}g.cursor--,g.bra=g.cursor;break;case 8:if(!g.in_grouping_b(f,97,246)||!g.out_grouping_b(f,97,246))return}g.slice_del(),i=!0}else g.limit_backward=r}(),g.cursor=g.limit,function(){var i,e,r;if(g.cursor>=t)if(e=g.limit_backward,g.limit_backward=t,g.ket=g.cursor,i=g.find_among_b(_,14)){if(g.bra=g.cursor,g.limit_backward=e,1==i){if(r=g.limit-g.cursor,g.eq_s_b(2,"po"))return;g.cursor=g.limit-r}g.slice_del()}else g.limit_backward=e}(),g.cursor=g.limit,i?(g.cursor>=s&&(e=g.limit_backward,g.limit_backward=s,g.ket=g.cursor,g.find_among_b(k,2)?(g.bra=g.cursor,g.limit_backward=e,g.slice_del()):g.limit_backward=e),g.cursor=g.limit):(g.cursor=g.limit,function(){var i,e,r,n,l,o;if(g.cursor>=s){if(e=g.limit_backward,g.limit_backward=s,g.ket=g.cursor,g.eq_s_b(1,"t")&&(g.bra=g.cursor,r=g.limit-g.cursor,g.in_grouping_b(f,97,246)&&(g.cursor=g.limit-r,g.slice_del(),g.limit_backward=e,n=g.limit-g.cursor,g.cursor>=t&&(g.cursor=t,l=g.limit_backward,g.limit_backward=g.cursor,g.cursor=g.limit-n,g.ket=g.cursor,i=g.find_among_b(b,2))))){if(g.bra=g.cursor,g.limit_backward=l,1==i){if(o=g.limit-g.cursor,g.eq_s_b(2,"po"))return;g.cursor=g.limit-o}return void g.slice_del()}g.limit_backward=e}}(),g.cursor=g.limit),function(){var i,e,r,t;if(g.cursor>=s){for(i=g.limit_backward,g.limit_backward=s,e=g.limit-g.cursor,v()&&(g.cursor=g.limit-e,g.ket=g.cursor,g.cursor>g.limit_backward&&(g.cursor--,g.bra=g.cursor,g.slice_del())),g.cursor=g.limit-e,g.ket=g.cursor,g.in_grouping_b(d,97,228)&&(g.bra=g.cursor,g.out_grouping_b(f,97,246)&&g.slice_del()),g.cursor=g.limit-e,g.ket=g.cursor,g.eq_s_b(1,"j")&&(g.bra=g.cursor,r=g.limit-g.cursor,g.eq_s_b(1,"o")?g.slice_del():(g.cursor=g.limit-r,g.eq_s_b(1,"u")&&g.slice_del())),g.cursor=g.limit-e,g.ket=g.cursor,g.eq_s_b(1,"o")&&(g.bra=g.cursor,g.eq_s_b(1,"j")&&g.slice_del()),g.cursor=g.limit-e,g.limit_backward=i;;){if(t=g.limit-g.cursor,g.out_grouping_b(f,97,246)){g.cursor=g.limit-t;break}if(g.cursor=g.limit-t,g.cursor<=g.limit_backward)return;g.cursor--}g.ket=g.cursor,g.cursor>g.limit_backward&&(g.cursor--,g.bra=g.cursor,n=g.slice_to(),g.eq_v_b(n)&&g.slice_del())}}(),!0}},function(i){return"function"==typeof i.update?i.update(function(i){return n.setCurrent(i),n.stem(),n.getCurrent()}):(n.setCurrent(i),n.stem(),n.getCurrent())}),i.Pipeline.registerFunction(i.fi.stemmer,"stemmer-fi"),i.fi.stopWordFilter=i.generateStopWordFilter("ei eivät emme en et ette että he heidän heidät heihin heille heillä heiltä heissä heistä heitä hän häneen hänelle hänellä häneltä hänen hänessä hänestä hänet häntä itse ja johon joiden joihin joiksi joilla joille joilta joina joissa joista joita joka joksi jolla jolle jolta jona jonka jos jossa josta jota jotka kanssa keiden keihin keiksi keille keillä keiltä keinä keissä keistä keitä keneen keneksi kenelle kenellä keneltä kenen kenenä kenessä kenestä kenet ketkä ketkä ketä koska kuin kuka kun me meidän meidät meihin meille meillä meiltä meissä meistä meitä mihin miksi mikä mille millä miltä minkä minkä minua minulla minulle minulta minun minussa minusta minut minuun minä minä missä mistä mitkä mitä mukaan mutta ne niiden niihin niiksi niille niillä niiltä niin niin niinä niissä niistä niitä noiden noihin noiksi noilla noille noilta noin noina noissa noista noita nuo nyt näiden näihin näiksi näille näillä näiltä näinä näissä näistä näitä nämä ole olemme olen olet olette oli olimme olin olisi olisimme olisin olisit olisitte olisivat olit olitte olivat olla olleet ollut on ovat poikki se sekä sen siihen siinä siitä siksi sille sillä sillä siltä sinua sinulla sinulle sinulta sinun sinussa sinusta sinut sinuun sinä sinä sitä tai te teidän teidät teihin teille teillä teiltä teissä teistä teitä tuo tuohon tuoksi tuolla tuolle tuolta tuon tuona tuossa tuosta tuota tähän täksi tälle tällä tältä tämä tämän tänä tässä tästä tätä vaan vai vaikka yli".split(" ")),i.Pipeline.registerFunction(i.fi.stopWordFilter,"stopWordFilter-fi")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.fr.js b/v1/assets/javascripts/lunr/lunr.fr.js new file mode 100644 index 000000000..3a9b9b177 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.fr.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r,s,i;e.fr=function(){this.pipeline.reset(),this.pipeline.add(e.fr.trimmer,e.fr.stopWordFilter,e.fr.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.fr.stemmer))},e.fr.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.fr.trimmer=e.trimmerSupport.generateTrimmer(e.fr.wordCharacters),e.Pipeline.registerFunction(e.fr.trimmer,"trimmer-fr"),e.fr.stemmer=(r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,i=new function(){var e,i,n,t=[new r("col",-1,-1),new r("par",-1,-1),new r("tap",-1,-1)],u=[new r("",-1,4),new r("I",0,1),new r("U",0,2),new r("Y",0,3)],o=[new r("iqU",-1,3),new r("abl",-1,3),new r("Ièr",-1,4),new r("ièr",-1,4),new r("eus",-1,2),new r("iv",-1,1)],c=[new r("ic",-1,2),new r("abil",-1,1),new r("iv",-1,3)],a=[new r("iqUe",-1,1),new r("atrice",-1,2),new r("ance",-1,1),new r("ence",-1,5),new r("logie",-1,3),new r("able",-1,1),new r("isme",-1,1),new r("euse",-1,11),new r("iste",-1,1),new r("ive",-1,8),new r("if",-1,8),new r("usion",-1,4),new r("ation",-1,2),new r("ution",-1,4),new r("ateur",-1,2),new r("iqUes",-1,1),new r("atrices",-1,2),new r("ances",-1,1),new r("ences",-1,5),new r("logies",-1,3),new r("ables",-1,1),new r("ismes",-1,1),new r("euses",-1,11),new r("istes",-1,1),new r("ives",-1,8),new r("ifs",-1,8),new r("usions",-1,4),new r("ations",-1,2),new r("utions",-1,4),new r("ateurs",-1,2),new r("ments",-1,15),new r("ements",30,6),new r("issements",31,12),new r("ités",-1,7),new r("ment",-1,15),new r("ement",34,6),new r("issement",35,12),new r("amment",34,13),new r("emment",34,14),new r("aux",-1,10),new r("eaux",39,9),new r("eux",-1,1),new r("ité",-1,7)],l=[new r("ira",-1,1),new r("ie",-1,1),new r("isse",-1,1),new r("issante",-1,1),new r("i",-1,1),new r("irai",4,1),new r("ir",-1,1),new r("iras",-1,1),new r("ies",-1,1),new r("îmes",-1,1),new r("isses",-1,1),new r("issantes",-1,1),new r("îtes",-1,1),new r("is",-1,1),new r("irais",13,1),new r("issais",13,1),new r("irions",-1,1),new r("issions",-1,1),new r("irons",-1,1),new r("issons",-1,1),new r("issants",-1,1),new r("it",-1,1),new r("irait",21,1),new r("issait",21,1),new r("issant",-1,1),new r("iraIent",-1,1),new r("issaIent",-1,1),new r("irent",-1,1),new r("issent",-1,1),new r("iront",-1,1),new r("ît",-1,1),new r("iriez",-1,1),new r("issiez",-1,1),new r("irez",-1,1),new r("issez",-1,1)],w=[new r("a",-1,3),new r("era",0,2),new r("asse",-1,3),new r("ante",-1,3),new r("ée",-1,2),new r("ai",-1,3),new r("erai",5,2),new r("er",-1,2),new r("as",-1,3),new r("eras",8,2),new r("âmes",-1,3),new r("asses",-1,3),new r("antes",-1,3),new r("âtes",-1,3),new r("ées",-1,2),new r("ais",-1,3),new r("erais",15,2),new r("ions",-1,1),new r("erions",17,2),new r("assions",17,3),new r("erons",-1,2),new r("ants",-1,3),new r("és",-1,2),new r("ait",-1,3),new r("erait",23,2),new r("ant",-1,3),new r("aIent",-1,3),new r("eraIent",26,2),new r("èrent",-1,2),new r("assent",-1,3),new r("eront",-1,2),new r("ât",-1,3),new r("ez",-1,2),new r("iez",32,2),new r("eriez",33,2),new r("assiez",33,3),new r("erez",32,2),new r("é",-1,2)],f=[new r("e",-1,3),new r("Ière",0,2),new r("ière",0,2),new r("ion",-1,1),new r("Ier",-1,2),new r("ier",-1,2),new r("ë",-1,4)],m=[new r("ell",-1,-1),new r("eill",-1,-1),new r("enn",-1,-1),new r("onn",-1,-1),new r("ett",-1,-1)],_=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,128,130,103,8,5],b=[1,65,20,0,0,0,0,0,0,0,0,0,0,0,0,0,128],d=new s;function k(e,r,s){return!(!d.eq_s(1,e)||(d.ket=d.cursor,!d.in_grouping(_,97,251)))&&(d.slice_from(r),d.cursor=s,!0)}function p(e,r,s){return!!d.eq_s(1,e)&&(d.ket=d.cursor,d.slice_from(r),d.cursor=s,!0)}function g(){for(;!d.in_grouping(_,97,251);){if(d.cursor>=d.limit)return!0;d.cursor++}for(;!d.out_grouping(_,97,251);){if(d.cursor>=d.limit)return!0;d.cursor++}return!1}function q(){return n<=d.cursor}function v(){return i<=d.cursor}function h(){return e<=d.cursor}function z(){if(!function(){var e,r;if(d.ket=d.cursor,e=d.find_among_b(a,43)){switch(d.bra=d.cursor,e){case 1:if(!h())return!1;d.slice_del();break;case 2:if(!h())return!1;d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"ic")&&(d.bra=d.cursor,h()?d.slice_del():d.slice_from("iqU"));break;case 3:if(!h())return!1;d.slice_from("log");break;case 4:if(!h())return!1;d.slice_from("u");break;case 5:if(!h())return!1;d.slice_from("ent");break;case 6:if(!q())return!1;if(d.slice_del(),d.ket=d.cursor,e=d.find_among_b(o,6))switch(d.bra=d.cursor,e){case 1:h()&&(d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"at")&&(d.bra=d.cursor,h()&&d.slice_del()));break;case 2:h()?d.slice_del():v()&&d.slice_from("eux");break;case 3:h()&&d.slice_del();break;case 4:q()&&d.slice_from("i")}break;case 7:if(!h())return!1;if(d.slice_del(),d.ket=d.cursor,e=d.find_among_b(c,3))switch(d.bra=d.cursor,e){case 1:h()?d.slice_del():d.slice_from("abl");break;case 2:h()?d.slice_del():d.slice_from("iqU");break;case 3:h()&&d.slice_del()}break;case 8:if(!h())return!1;if(d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"at")&&(d.bra=d.cursor,h()&&(d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"ic")))){d.bra=d.cursor,h()?d.slice_del():d.slice_from("iqU");break}break;case 9:d.slice_from("eau");break;case 10:if(!v())return!1;d.slice_from("al");break;case 11:if(h())d.slice_del();else{if(!v())return!1;d.slice_from("eux")}break;case 12:if(!v()||!d.out_grouping_b(_,97,251))return!1;d.slice_del();break;case 13:return q()&&d.slice_from("ant"),!1;case 14:return q()&&d.slice_from("ent"),!1;case 15:return r=d.limit-d.cursor,d.in_grouping_b(_,97,251)&&q()&&(d.cursor=d.limit-r,d.slice_del()),!1}return!0}return!1}()&&(d.cursor=d.limit,!function(){var e,r;if(d.cursor=n){if(s=d.limit_backward,d.limit_backward=n,d.ket=d.cursor,e=d.find_among_b(f,7))switch(d.bra=d.cursor,e){case 1:if(h()){if(i=d.limit-d.cursor,!d.eq_s_b(1,"s")&&(d.cursor=d.limit-i,!d.eq_s_b(1,"t")))break;d.slice_del()}break;case 2:d.slice_from("i");break;case 3:d.slice_del();break;case 4:d.eq_s_b(2,"gu")&&d.slice_del()}d.limit_backward=s}}();d.cursor=d.limit,d.ket=d.cursor,d.eq_s_b(1,"Y")?(d.bra=d.cursor,d.slice_from("i")):(d.cursor=d.limit,d.eq_s_b(1,"ç")&&(d.bra=d.cursor,d.slice_from("c")))}this.setCurrent=function(e){d.setCurrent(e)},this.getCurrent=function(){return d.getCurrent()},this.stem=function(){var r,s=d.cursor;return function(){for(var e,r;;){if(e=d.cursor,d.in_grouping(_,97,251)){if(d.bra=d.cursor,r=d.cursor,k("u","U",e))continue;if(d.cursor=r,k("i","I",e))continue;if(d.cursor=r,p("y","Y",e))continue}if(d.cursor=e,d.bra=e,!k("y","Y",e)){if(d.cursor=e,d.eq_s(1,"q")&&(d.bra=d.cursor,p("u","U",e)))continue;if(d.cursor=e,e>=d.limit)return;d.cursor++}}}(),d.cursor=s,function(){var r=d.cursor;if(n=d.limit,i=n,e=n,d.in_grouping(_,97,251)&&d.in_grouping(_,97,251)&&d.cursor=d.limit){d.cursor=n;break}d.cursor++}while(!d.in_grouping(_,97,251))}n=d.cursor,d.cursor=r,g()||(i=d.cursor,g()||(e=d.cursor))}(),d.limit_backward=s,d.cursor=d.limit,z(),d.cursor=d.limit,r=d.limit-d.cursor,d.find_among_b(m,5)&&(d.cursor=d.limit-r,d.ket=d.cursor,d.cursor>d.limit_backward&&(d.cursor--,d.bra=d.cursor,d.slice_del())),d.cursor=d.limit,function(){for(var e,r=1;d.out_grouping_b(_,97,251);)r--;if(r<=0){if(d.ket=d.cursor,e=d.limit-d.cursor,!d.eq_s_b(1,"é")&&(d.cursor=d.limit-e,!d.eq_s_b(1,"è")))return;d.bra=d.cursor,d.slice_from("e")}}(),d.cursor=d.limit_backward,function(){for(var e,r;r=d.cursor,d.bra=r,e=d.find_among(u,4);)switch(d.ket=d.cursor,e){case 1:d.slice_from("i");break;case 2:d.slice_from("u");break;case 3:d.slice_from("y");break;case 4:if(d.cursor>=d.limit)return;d.cursor++}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}),e.Pipeline.registerFunction(e.fr.stemmer,"stemmer-fr"),e.fr.stopWordFilter=e.generateStopWordFilter("ai aie aient aies ait as au aura aurai auraient aurais aurait auras aurez auriez aurions aurons auront aux avaient avais avait avec avez aviez avions avons ayant ayez ayons c ce ceci celà ces cet cette d dans de des du elle en es est et eu eue eues eurent eus eusse eussent eusses eussiez eussions eut eux eûmes eût eûtes furent fus fusse fussent fusses fussiez fussions fut fûmes fût fûtes ici il ils j je l la le les leur leurs lui m ma mais me mes moi mon même n ne nos notre nous on ont ou par pas pour qu que quel quelle quelles quels qui s sa sans se sera serai seraient serais serait seras serez seriez serions serons seront ses soi soient sois soit sommes son sont soyez soyons suis sur t ta te tes toi ton tu un une vos votre vous y à étaient étais était étant étiez étions été étée étées étés êtes".split(" ")),e.Pipeline.registerFunction(e.fr.stopWordFilter,"stopWordFilter-fr")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.hu.js b/v1/assets/javascripts/lunr/lunr.hu.js new file mode 100644 index 000000000..fa704a69c --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.hu.js @@ -0,0 +1 @@ +!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var n,r,i;e.hu=function(){this.pipeline.reset(),this.pipeline.add(e.hu.trimmer,e.hu.stopWordFilter,e.hu.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hu.stemmer))},e.hu.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.hu.trimmer=e.trimmerSupport.generateTrimmer(e.hu.wordCharacters),e.Pipeline.registerFunction(e.hu.trimmer,"trimmer-hu"),e.hu.stemmer=(n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,i=new function(){var e,i=[new n("cs",-1,-1),new n("dzs",-1,-1),new n("gy",-1,-1),new n("ly",-1,-1),new n("ny",-1,-1),new n("sz",-1,-1),new n("ty",-1,-1),new n("zs",-1,-1)],a=[new n("á",-1,1),new n("é",-1,2)],t=[new n("bb",-1,-1),new n("cc",-1,-1),new n("dd",-1,-1),new n("ff",-1,-1),new n("gg",-1,-1),new n("jj",-1,-1),new n("kk",-1,-1),new n("ll",-1,-1),new n("mm",-1,-1),new n("nn",-1,-1),new n("pp",-1,-1),new n("rr",-1,-1),new n("ccs",-1,-1),new n("ss",-1,-1),new n("zzs",-1,-1),new n("tt",-1,-1),new n("vv",-1,-1),new n("ggy",-1,-1),new n("lly",-1,-1),new n("nny",-1,-1),new n("tty",-1,-1),new n("ssz",-1,-1),new n("zz",-1,-1)],s=[new n("al",-1,1),new n("el",-1,2)],c=[new n("ba",-1,-1),new n("ra",-1,-1),new n("be",-1,-1),new n("re",-1,-1),new n("ig",-1,-1),new n("nak",-1,-1),new n("nek",-1,-1),new n("val",-1,-1),new n("vel",-1,-1),new n("ul",-1,-1),new n("nál",-1,-1),new n("nél",-1,-1),new n("ból",-1,-1),new n("ról",-1,-1),new n("tól",-1,-1),new n("bõl",-1,-1),new n("rõl",-1,-1),new n("tõl",-1,-1),new n("ül",-1,-1),new n("n",-1,-1),new n("an",19,-1),new n("ban",20,-1),new n("en",19,-1),new n("ben",22,-1),new n("képpen",22,-1),new n("on",19,-1),new n("ön",19,-1),new n("képp",-1,-1),new n("kor",-1,-1),new n("t",-1,-1),new n("at",29,-1),new n("et",29,-1),new n("ként",29,-1),new n("anként",32,-1),new n("enként",32,-1),new n("onként",32,-1),new n("ot",29,-1),new n("ért",29,-1),new n("öt",29,-1),new n("hez",-1,-1),new n("hoz",-1,-1),new n("höz",-1,-1),new n("vá",-1,-1),new n("vé",-1,-1)],w=[new n("án",-1,2),new n("én",-1,1),new n("ánként",-1,3)],o=[new n("stul",-1,2),new n("astul",0,1),new n("ástul",0,3),new n("stül",-1,2),new n("estül",3,1),new n("éstül",3,4)],l=[new n("á",-1,1),new n("é",-1,2)],u=[new n("k",-1,7),new n("ak",0,4),new n("ek",0,6),new n("ok",0,5),new n("ák",0,1),new n("ék",0,2),new n("ök",0,3)],m=[new n("éi",-1,7),new n("áéi",0,6),new n("ééi",0,5),new n("é",-1,9),new n("ké",3,4),new n("aké",4,1),new n("eké",4,1),new n("oké",4,1),new n("áké",4,3),new n("éké",4,2),new n("öké",4,1),new n("éé",3,8)],k=[new n("a",-1,18),new n("ja",0,17),new n("d",-1,16),new n("ad",2,13),new n("ed",2,13),new n("od",2,13),new n("ád",2,14),new n("éd",2,15),new n("öd",2,13),new n("e",-1,18),new n("je",9,17),new n("nk",-1,4),new n("unk",11,1),new n("ánk",11,2),new n("énk",11,3),new n("ünk",11,1),new n("uk",-1,8),new n("juk",16,7),new n("ájuk",17,5),new n("ük",-1,8),new n("jük",19,7),new n("éjük",20,6),new n("m",-1,12),new n("am",22,9),new n("em",22,9),new n("om",22,9),new n("ám",22,10),new n("ém",22,11),new n("o",-1,18),new n("á",-1,19),new n("é",-1,20)],f=[new n("id",-1,10),new n("aid",0,9),new n("jaid",1,6),new n("eid",0,9),new n("jeid",3,6),new n("áid",0,7),new n("éid",0,8),new n("i",-1,15),new n("ai",7,14),new n("jai",8,11),new n("ei",7,14),new n("jei",10,11),new n("ái",7,12),new n("éi",7,13),new n("itek",-1,24),new n("eitek",14,21),new n("jeitek",15,20),new n("éitek",14,23),new n("ik",-1,29),new n("aik",18,26),new n("jaik",19,25),new n("eik",18,26),new n("jeik",21,25),new n("áik",18,27),new n("éik",18,28),new n("ink",-1,20),new n("aink",25,17),new n("jaink",26,16),new n("eink",25,17),new n("jeink",28,16),new n("áink",25,18),new n("éink",25,19),new n("aitok",-1,21),new n("jaitok",32,20),new n("áitok",-1,22),new n("im",-1,5),new n("aim",35,4),new n("jaim",36,1),new n("eim",35,4),new n("jeim",38,1),new n("áim",35,2),new n("éim",35,3)],b=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,1,17,52,14],d=new r;function g(){return e<=d.cursor}function h(){var e=d.limit-d.cursor;return!!d.find_among_b(t,23)&&(d.cursor=d.limit-e,!0)}function p(){if(d.cursor>d.limit_backward){d.cursor--,d.ket=d.cursor;var e=d.cursor-1;d.limit_backward<=e&&e<=d.limit&&(d.cursor=e,d.bra=e,d.slice_del())}}function _(){d.ket=d.cursor,d.find_among_b(c,44)&&(d.bra=d.cursor,g()&&(d.slice_del(),function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(a,2))&&(d.bra=d.cursor,g()))switch(e){case 1:d.slice_from("a");break;case 2:d.slice_from("e")}}()))}this.setCurrent=function(e){d.setCurrent(e)},this.getCurrent=function(){return d.getCurrent()},this.stem=function(){var n=d.cursor;return function(){var n,r=d.cursor;if(e=d.limit,d.in_grouping(b,97,252))for(;;){if(n=d.cursor,d.out_grouping(b,97,252))return d.cursor=n,d.find_among(i,8)||(d.cursor=n,n=d.limit)return void(e=n);d.cursor++}if(d.cursor=r,d.out_grouping(b,97,252)){for(;!d.in_grouping(b,97,252);){if(d.cursor>=d.limit)return;d.cursor++}e=d.cursor}}(),d.limit_backward=n,d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(s,2))&&(d.bra=d.cursor,g())){if((1==e||2==e)&&!h())return;d.slice_del(),p()}}(),d.cursor=d.limit,_(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(w,3))&&(d.bra=d.cursor,g()))switch(e){case 1:d.slice_from("e");break;case 2:case 3:d.slice_from("a")}}(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(o,6))&&(d.bra=d.cursor,g()))switch(e){case 1:case 2:d.slice_del();break;case 3:d.slice_from("a");break;case 4:d.slice_from("e")}}(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(l,2))&&(d.bra=d.cursor,g())){if((1==e||2==e)&&!h())return;d.slice_del(),p()}}(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(m,12))&&(d.bra=d.cursor,g()))switch(e){case 1:case 4:case 7:case 9:d.slice_del();break;case 2:case 5:case 8:d.slice_from("e");break;case 3:case 6:d.slice_from("a")}}(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(k,31))&&(d.bra=d.cursor,g()))switch(e){case 1:case 4:case 7:case 8:case 9:case 12:case 13:case 16:case 17:case 18:d.slice_del();break;case 2:case 5:case 10:case 14:case 19:d.slice_from("a");break;case 3:case 6:case 11:case 15:case 20:d.slice_from("e")}}(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(f,42))&&(d.bra=d.cursor,g()))switch(e){case 1:case 4:case 5:case 6:case 9:case 10:case 11:case 14:case 15:case 16:case 17:case 20:case 21:case 24:case 25:case 26:case 29:d.slice_del();break;case 2:case 7:case 12:case 18:case 22:case 27:d.slice_from("a");break;case 3:case 8:case 13:case 19:case 23:case 28:d.slice_from("e")}}(),d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,(e=d.find_among_b(u,7))&&(d.bra=d.cursor,g()))switch(e){case 1:d.slice_from("a");break;case 2:d.slice_from("e");break;case 3:case 4:case 5:case 6:case 7:d.slice_del()}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}),e.Pipeline.registerFunction(e.hu.stemmer,"stemmer-hu"),e.hu.stopWordFilter=e.generateStopWordFilter("a abban ahhoz ahogy ahol aki akik akkor alatt amely amelyek amelyekben amelyeket amelyet amelynek ami amikor amit amolyan amíg annak arra arról az azok azon azonban azt aztán azután azzal azért be belül benne bár cikk cikkek cikkeket csak de e ebben eddig egy egyes egyetlen egyik egyre egyéb egész ehhez ekkor el ellen elsõ elég elõ elõször elõtt emilyen ennek erre ez ezek ezen ezt ezzel ezért fel felé hanem hiszen hogy hogyan igen ill ill. illetve ilyen ilyenkor ismét ison itt jobban jó jól kell kellett keressünk keresztül ki kívül között közül legalább legyen lehet lehetett lenne lenni lesz lett maga magát majd majd meg mellett mely melyek mert mi mikor milyen minden mindenki mindent mindig mint mintha mit mivel miért most már más másik még míg nagy nagyobb nagyon ne nekem neki nem nincs néha néhány nélkül olyan ott pedig persze rá s saját sem semmi sok sokat sokkal szemben szerint szinte számára talán tehát teljes tovább továbbá több ugyanis utolsó után utána vagy vagyis vagyok valaki valami valamint való van vannak vele vissza viszont volna volt voltak voltam voltunk által általában át én éppen és így õ õk õket össze úgy új újabb újra".split(" ")),e.Pipeline.registerFunction(e.hu.stopWordFilter,"stopWordFilter-hu")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.it.js b/v1/assets/javascripts/lunr/lunr.it.js new file mode 100644 index 000000000..293073389 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.it.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r,i,n;e.it=function(){this.pipeline.reset(),this.pipeline.add(e.it.trimmer,e.it.stopWordFilter,e.it.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.it.stemmer))},e.it.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.it.trimmer=e.trimmerSupport.generateTrimmer(e.it.wordCharacters),e.Pipeline.registerFunction(e.it.trimmer,"trimmer-it"),e.it.stemmer=(r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){var e,n,o,t=[new r("",-1,7),new r("qu",0,6),new r("á",0,1),new r("é",0,2),new r("í",0,3),new r("ó",0,4),new r("ú",0,5)],s=[new r("",-1,3),new r("I",0,1),new r("U",0,2)],a=[new r("la",-1,-1),new r("cela",0,-1),new r("gliela",0,-1),new r("mela",0,-1),new r("tela",0,-1),new r("vela",0,-1),new r("le",-1,-1),new r("cele",6,-1),new r("gliele",6,-1),new r("mele",6,-1),new r("tele",6,-1),new r("vele",6,-1),new r("ne",-1,-1),new r("cene",12,-1),new r("gliene",12,-1),new r("mene",12,-1),new r("sene",12,-1),new r("tene",12,-1),new r("vene",12,-1),new r("ci",-1,-1),new r("li",-1,-1),new r("celi",20,-1),new r("glieli",20,-1),new r("meli",20,-1),new r("teli",20,-1),new r("veli",20,-1),new r("gli",20,-1),new r("mi",-1,-1),new r("si",-1,-1),new r("ti",-1,-1),new r("vi",-1,-1),new r("lo",-1,-1),new r("celo",31,-1),new r("glielo",31,-1),new r("melo",31,-1),new r("telo",31,-1),new r("velo",31,-1)],u=[new r("ando",-1,1),new r("endo",-1,1),new r("ar",-1,2),new r("er",-1,2),new r("ir",-1,2)],c=[new r("ic",-1,-1),new r("abil",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],w=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],l=[new r("ica",-1,1),new r("logia",-1,3),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,9),new r("anza",-1,1),new r("enza",-1,5),new r("ice",-1,1),new r("atrice",7,1),new r("iche",-1,1),new r("logie",-1,3),new r("abile",-1,1),new r("ibile",-1,1),new r("usione",-1,4),new r("azione",-1,2),new r("uzione",-1,4),new r("atore",-1,2),new r("ose",-1,1),new r("ante",-1,1),new r("mente",-1,1),new r("amente",19,7),new r("iste",-1,1),new r("ive",-1,9),new r("anze",-1,1),new r("enze",-1,5),new r("ici",-1,1),new r("atrici",25,1),new r("ichi",-1,1),new r("abili",-1,1),new r("ibili",-1,1),new r("ismi",-1,1),new r("usioni",-1,4),new r("azioni",-1,2),new r("uzioni",-1,4),new r("atori",-1,2),new r("osi",-1,1),new r("anti",-1,1),new r("amenti",-1,6),new r("imenti",-1,6),new r("isti",-1,1),new r("ivi",-1,9),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,6),new r("imento",-1,6),new r("ivo",-1,9),new r("ità",-1,8),new r("istà",-1,1),new r("istè",-1,1),new r("istì",-1,1)],m=[new r("isca",-1,1),new r("enda",-1,1),new r("ata",-1,1),new r("ita",-1,1),new r("uta",-1,1),new r("ava",-1,1),new r("eva",-1,1),new r("iva",-1,1),new r("erebbe",-1,1),new r("irebbe",-1,1),new r("isce",-1,1),new r("ende",-1,1),new r("are",-1,1),new r("ere",-1,1),new r("ire",-1,1),new r("asse",-1,1),new r("ate",-1,1),new r("avate",16,1),new r("evate",16,1),new r("ivate",16,1),new r("ete",-1,1),new r("erete",20,1),new r("irete",20,1),new r("ite",-1,1),new r("ereste",-1,1),new r("ireste",-1,1),new r("ute",-1,1),new r("erai",-1,1),new r("irai",-1,1),new r("isci",-1,1),new r("endi",-1,1),new r("erei",-1,1),new r("irei",-1,1),new r("assi",-1,1),new r("ati",-1,1),new r("iti",-1,1),new r("eresti",-1,1),new r("iresti",-1,1),new r("uti",-1,1),new r("avi",-1,1),new r("evi",-1,1),new r("ivi",-1,1),new r("isco",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("Yamo",-1,1),new r("iamo",-1,1),new r("avamo",-1,1),new r("evamo",-1,1),new r("ivamo",-1,1),new r("eremo",-1,1),new r("iremo",-1,1),new r("assimo",-1,1),new r("ammo",-1,1),new r("emmo",-1,1),new r("eremmo",54,1),new r("iremmo",54,1),new r("immo",-1,1),new r("ano",-1,1),new r("iscano",58,1),new r("avano",58,1),new r("evano",58,1),new r("ivano",58,1),new r("eranno",-1,1),new r("iranno",-1,1),new r("ono",-1,1),new r("iscono",65,1),new r("arono",65,1),new r("erono",65,1),new r("irono",65,1),new r("erebbero",-1,1),new r("irebbero",-1,1),new r("assero",-1,1),new r("essero",-1,1),new r("issero",-1,1),new r("ato",-1,1),new r("ito",-1,1),new r("uto",-1,1),new r("avo",-1,1),new r("evo",-1,1),new r("ivo",-1,1),new r("ar",-1,1),new r("ir",-1,1),new r("erà",-1,1),new r("irà",-1,1),new r("erò",-1,1),new r("irò",-1,1)],f=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,128,128,8,2,1],v=[17,65,0,0,0,0,0,0,0,0,0,0,0,0,0,128,128,8,2],b=[17],d=new i;function _(e,r,i){return!(!d.eq_s(1,e)||(d.ket=d.cursor,!d.in_grouping(f,97,249)))&&(d.slice_from(r),d.cursor=i,!0)}function g(e){if(d.cursor=e,!d.in_grouping(f,97,249))return!1;for(;!d.out_grouping(f,97,249);){if(d.cursor>=d.limit)return!1;d.cursor++}return!0}function p(){var e,r=d.cursor;if(!function(){if(d.in_grouping(f,97,249)){var e=d.cursor;if(d.out_grouping(f,97,249)){for(;!d.in_grouping(f,97,249);){if(d.cursor>=d.limit)return g(e);d.cursor++}return!0}return g(e)}return!1}()){if(d.cursor=r,!d.out_grouping(f,97,249))return;if(e=d.cursor,d.out_grouping(f,97,249)){for(;!d.in_grouping(f,97,249);){if(d.cursor>=d.limit)return d.cursor=e,void(d.in_grouping(f,97,249)&&d.cursor=d.limit)return;d.cursor++}o=d.cursor}function k(){for(;!d.in_grouping(f,97,249);){if(d.cursor>=d.limit)return!1;d.cursor++}for(;!d.out_grouping(f,97,249);){if(d.cursor>=d.limit)return!1;d.cursor++}return!0}function h(){return o<=d.cursor}function q(){return e<=d.cursor}function C(){var e;if(d.ket=d.cursor,!(e=d.find_among_b(l,51)))return!1;switch(d.bra=d.cursor,e){case 1:if(!q())return!1;d.slice_del();break;case 2:if(!q())return!1;d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"ic")&&(d.bra=d.cursor,q()&&d.slice_del());break;case 3:if(!q())return!1;d.slice_from("log");break;case 4:if(!q())return!1;d.slice_from("u");break;case 5:if(!q())return!1;d.slice_from("ente");break;case 6:if(!h())return!1;d.slice_del();break;case 7:if(!(n<=d.cursor))return!1;d.slice_del(),d.ket=d.cursor,(e=d.find_among_b(c,4))&&(d.bra=d.cursor,q()&&(d.slice_del(),1==e&&(d.ket=d.cursor,d.eq_s_b(2,"at")&&(d.bra=d.cursor,q()&&d.slice_del()))));break;case 8:if(!q())return!1;d.slice_del(),d.ket=d.cursor,(e=d.find_among_b(w,3))&&(d.bra=d.cursor,1==e&&q()&&d.slice_del());break;case 9:if(!q())return!1;d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"at")&&(d.bra=d.cursor,q()&&(d.slice_del(),d.ket=d.cursor,d.eq_s_b(2,"ic")&&(d.bra=d.cursor,q()&&d.slice_del())))}return!0}function z(){var e;e=d.limit-d.cursor,d.ket=d.cursor,d.in_grouping_b(v,97,242)&&(d.bra=d.cursor,h()&&(d.slice_del(),d.ket=d.cursor,d.eq_s_b(1,"i")&&(d.bra=d.cursor,h())))?d.slice_del():d.cursor=d.limit-e,d.ket=d.cursor,d.eq_s_b(1,"h")&&(d.bra=d.cursor,d.in_grouping_b(b,99,103)&&h()&&d.slice_del())}this.setCurrent=function(e){d.setCurrent(e)},this.getCurrent=function(){return d.getCurrent()},this.stem=function(){var r,i,c,w=d.cursor;return function(){for(var e,r,i,n,o=d.cursor;;){if(d.bra=d.cursor,e=d.find_among(t,7))switch(d.ket=d.cursor,e){case 1:d.slice_from("à");continue;case 2:d.slice_from("è");continue;case 3:d.slice_from("ì");continue;case 4:d.slice_from("ò");continue;case 5:d.slice_from("ù");continue;case 6:d.slice_from("qU");continue;case 7:if(d.cursor>=d.limit)break;d.cursor++;continue}break}for(d.cursor=o;;)for(r=d.cursor;;){if(i=d.cursor,d.in_grouping(f,97,249)){if(d.bra=d.cursor,n=d.cursor,_("u","U",i))break;if(d.cursor=n,_("i","I",i))break}if(d.cursor=i,d.cursor>=d.limit)return void(d.cursor=r);d.cursor++}}(),d.cursor=w,r=d.cursor,o=d.limit,n=o,e=o,p(),d.cursor=r,k()&&(n=d.cursor,k()&&(e=d.cursor)),d.limit_backward=w,d.cursor=d.limit,function(){var e;if(d.ket=d.cursor,d.find_among_b(a,37)&&(d.bra=d.cursor,(e=d.find_among_b(u,5))&&h()))switch(e){case 1:d.slice_del();break;case 2:d.slice_from("e")}}(),d.cursor=d.limit,C()||(d.cursor=d.limit,d.cursor>=o&&(c=d.limit_backward,d.limit_backward=o,d.ket=d.cursor,(i=d.find_among_b(m,87))&&(d.bra=d.cursor,1==i&&d.slice_del()),d.limit_backward=c)),d.cursor=d.limit,z(),d.cursor=d.limit_backward,function(){for(var e;d.bra=d.cursor,e=d.find_among(s,3);)switch(d.ket=d.cursor,e){case 1:d.slice_from("i");break;case 2:d.slice_from("u");break;case 3:if(d.cursor>=d.limit)return;d.cursor++}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}),e.Pipeline.registerFunction(e.it.stemmer,"stemmer-it"),e.it.stopWordFilter=e.generateStopWordFilter("a abbia abbiamo abbiano abbiate ad agl agli ai al all alla alle allo anche avemmo avendo avesse avessero avessi avessimo aveste avesti avete aveva avevamo avevano avevate avevi avevo avrai avranno avrebbe avrebbero avrei avremmo avremo avreste avresti avrete avrà avrò avuta avute avuti avuto c che chi ci coi col come con contro cui da dagl dagli dai dal dall dalla dalle dallo degl degli dei del dell della delle dello di dov dove e ebbe ebbero ebbi ed era erano eravamo eravate eri ero essendo faccia facciamo facciano facciate faccio facemmo facendo facesse facessero facessi facessimo faceste facesti faceva facevamo facevano facevate facevi facevo fai fanno farai faranno farebbe farebbero farei faremmo faremo fareste faresti farete farà farò fece fecero feci fosse fossero fossi fossimo foste fosti fu fui fummo furono gli ha hai hanno ho i il in io l la le lei li lo loro lui ma mi mia mie miei mio ne negl negli nei nel nell nella nelle nello noi non nostra nostre nostri nostro o per perché più quale quanta quante quanti quanto quella quelle quelli quello questa queste questi questo sarai saranno sarebbe sarebbero sarei saremmo saremo sareste saresti sarete sarà sarò se sei si sia siamo siano siate siete sono sta stai stando stanno starai staranno starebbe starebbero starei staremmo staremo stareste staresti starete starà starò stava stavamo stavano stavate stavi stavo stemmo stesse stessero stessi stessimo steste stesti stette stettero stetti stia stiamo stiano stiate sto su sua sue sugl sugli sui sul sull sulla sulle sullo suo suoi ti tra tu tua tue tuo tuoi tutti tutto un una uno vi voi vostra vostre vostri vostro è".split(" ")),e.Pipeline.registerFunction(e.it.stopWordFilter,"stopWordFilter-it")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.jp.js b/v1/assets/javascripts/lunr/lunr.jp.js new file mode 100644 index 000000000..a33c3c71c --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.jp.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.jp=function(){this.pipeline.reset(),this.pipeline.add(e.jp.stopWordFilter,e.jp.stemmer),r?this.tokenizer=e.jp.tokenizer:(e.tokenizer&&(e.tokenizer=e.jp.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.jp.tokenizer))};var t=new e.TinySegmenter;e.jp.tokenizer=function(n){if(!arguments.length||null==n||null==n)return[];if(Array.isArray(n))return n.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(var i=n.toString().toLowerCase().replace(/^\s+/,""),o=i.length-1;o>=0;o--)if(/\S/.test(i.charAt(o))){i=i.substring(0,o+1);break}return t.segment(i).filter(function(e){return!!e}).map(function(t){return r?new e.Token(t):t})},e.jp.stemmer=function(e){return e},e.Pipeline.registerFunction(e.jp.stemmer,"stemmer-jp"),e.jp.wordCharacters="一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーア-ン゙a-zA-Za-zA-Z0-90-9",e.jp.stopWordFilter=function(t){if(-1===e.jp.stopWordFilter.stopWords.indexOf(r?t.toString():t))return t},e.jp.stopWordFilter=e.generateStopWordFilter("これ それ あれ この その あの ここ そこ あそこ こちら どこ だれ なに なん 何 私 貴方 貴方方 我々 私達 あの人 あのかた 彼女 彼 です あります おります います は が の に を で え から まで より も どの と し それで しかし".split(" ")),e.Pipeline.registerFunction(e.jp.stopWordFilter,"stopWordFilter-jp")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.multi.js b/v1/assets/javascripts/lunr/lunr.multi.js new file mode 100644 index 000000000..d3dbc860c --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.multi.js @@ -0,0 +1 @@ +!function(e,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(e.lunr)}(this,function(){return function(e){e.multiLanguage=function(){for(var i=Array.prototype.slice.call(arguments),t=i.join("-"),r="",n=[],s=[],p=0;p=l.limit)return;l.cursor=r+1}for(;!l.out_grouping(a,97,248);){if(l.cursor>=l.limit)return;l.cursor++}(i=l.cursor)=i&&(r=l.limit_backward,l.limit_backward=i,l.ket=l.cursor,e=l.find_among_b(t,29),l.limit_backward=r,e))switch(l.bra=l.cursor,e){case 1:l.slice_del();break;case 2:n=l.limit-l.cursor,l.in_grouping_b(m,98,122)?l.slice_del():(l.cursor=l.limit-n,l.eq_s_b(1,"k")&&l.out_grouping_b(a,97,248)&&l.slice_del());break;case 3:l.slice_from("er")}}(),l.cursor=l.limit,n=l.limit-l.cursor,l.cursor>=i&&(r=l.limit_backward,l.limit_backward=i,l.ket=l.cursor,l.find_among_b(o,2)?(l.bra=l.cursor,l.limit_backward=r,l.cursor=l.limit-n,l.cursor>l.limit_backward&&(l.cursor--,l.bra=l.cursor,l.slice_del())):l.limit_backward=r),l.cursor=l.limit,l.cursor>=i&&(d=l.limit_backward,l.limit_backward=i,l.ket=l.cursor,(u=l.find_among_b(s,11))?(l.bra=l.cursor,l.limit_backward=d,1==u&&l.slice_del()):l.limit_backward=d),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.pt.js b/v1/assets/javascripts/lunr/lunr.pt.js new file mode 100644 index 000000000..51035c969 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.pt.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r,s,n;e.pt=function(){this.pipeline.reset(),this.pipeline.add(e.pt.trimmer,e.pt.stopWordFilter,e.pt.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.pt.stemmer))},e.pt.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.pt.trimmer=e.trimmerSupport.generateTrimmer(e.pt.wordCharacters),e.Pipeline.registerFunction(e.pt.trimmer,"trimmer-pt"),e.pt.stemmer=(r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,n=new function(){var e,n,i,o=[new r("",-1,3),new r("ã",0,1),new r("õ",0,2)],a=[new r("",-1,3),new r("a~",0,1),new r("o~",0,2)],t=[new r("ic",-1,-1),new r("ad",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],u=[new r("ante",-1,1),new r("avel",-1,1),new r("ível",-1,1)],w=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],m=[new r("ica",-1,1),new r("ância",-1,1),new r("ência",-1,4),new r("ira",-1,9),new r("adora",-1,1),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,8),new r("eza",-1,1),new r("logía",-1,2),new r("idade",-1,7),new r("ante",-1,1),new r("mente",-1,6),new r("amente",12,5),new r("ável",-1,1),new r("ível",-1,1),new r("ución",-1,3),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,1),new r("imento",-1,1),new r("ivo",-1,8),new r("aça~o",-1,1),new r("ador",-1,1),new r("icas",-1,1),new r("ências",-1,4),new r("iras",-1,9),new r("adoras",-1,1),new r("osas",-1,1),new r("istas",-1,1),new r("ivas",-1,8),new r("ezas",-1,1),new r("logías",-1,2),new r("idades",-1,7),new r("uciones",-1,3),new r("adores",-1,1),new r("antes",-1,1),new r("aço~es",-1,1),new r("icos",-1,1),new r("ismos",-1,1),new r("osos",-1,1),new r("amentos",-1,1),new r("imentos",-1,1),new r("ivos",-1,8)],c=[new r("ada",-1,1),new r("ida",-1,1),new r("ia",-1,1),new r("aria",2,1),new r("eria",2,1),new r("iria",2,1),new r("ara",-1,1),new r("era",-1,1),new r("ira",-1,1),new r("ava",-1,1),new r("asse",-1,1),new r("esse",-1,1),new r("isse",-1,1),new r("aste",-1,1),new r("este",-1,1),new r("iste",-1,1),new r("ei",-1,1),new r("arei",16,1),new r("erei",16,1),new r("irei",16,1),new r("am",-1,1),new r("iam",20,1),new r("ariam",21,1),new r("eriam",21,1),new r("iriam",21,1),new r("aram",20,1),new r("eram",20,1),new r("iram",20,1),new r("avam",20,1),new r("em",-1,1),new r("arem",29,1),new r("erem",29,1),new r("irem",29,1),new r("assem",29,1),new r("essem",29,1),new r("issem",29,1),new r("ado",-1,1),new r("ido",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("indo",-1,1),new r("ara~o",-1,1),new r("era~o",-1,1),new r("ira~o",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("ir",-1,1),new r("as",-1,1),new r("adas",47,1),new r("idas",47,1),new r("ias",47,1),new r("arias",50,1),new r("erias",50,1),new r("irias",50,1),new r("aras",47,1),new r("eras",47,1),new r("iras",47,1),new r("avas",47,1),new r("es",-1,1),new r("ardes",58,1),new r("erdes",58,1),new r("irdes",58,1),new r("ares",58,1),new r("eres",58,1),new r("ires",58,1),new r("asses",58,1),new r("esses",58,1),new r("isses",58,1),new r("astes",58,1),new r("estes",58,1),new r("istes",58,1),new r("is",-1,1),new r("ais",71,1),new r("eis",71,1),new r("areis",73,1),new r("ereis",73,1),new r("ireis",73,1),new r("áreis",73,1),new r("éreis",73,1),new r("íreis",73,1),new r("ásseis",73,1),new r("ésseis",73,1),new r("ísseis",73,1),new r("áveis",73,1),new r("íeis",73,1),new r("aríeis",84,1),new r("eríeis",84,1),new r("iríeis",84,1),new r("ados",-1,1),new r("idos",-1,1),new r("amos",-1,1),new r("áramos",90,1),new r("éramos",90,1),new r("íramos",90,1),new r("ávamos",90,1),new r("íamos",90,1),new r("aríamos",95,1),new r("eríamos",95,1),new r("iríamos",95,1),new r("emos",-1,1),new r("aremos",99,1),new r("eremos",99,1),new r("iremos",99,1),new r("ássemos",99,1),new r("êssemos",99,1),new r("íssemos",99,1),new r("imos",-1,1),new r("armos",-1,1),new r("ermos",-1,1),new r("irmos",-1,1),new r("ámos",-1,1),new r("arás",-1,1),new r("erás",-1,1),new r("irás",-1,1),new r("eu",-1,1),new r("iu",-1,1),new r("ou",-1,1),new r("ará",-1,1),new r("erá",-1,1),new r("irá",-1,1)],l=[new r("a",-1,1),new r("i",-1,1),new r("o",-1,1),new r("os",-1,1),new r("á",-1,1),new r("í",-1,1),new r("ó",-1,1)],f=[new r("e",-1,1),new r("ç",-1,2),new r("é",-1,1),new r("ê",-1,1)],d=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,3,19,12,2],v=new s;function p(){if(v.out_grouping(d,97,250)){for(;!v.in_grouping(d,97,250);){if(v.cursor>=v.limit)return!0;v.cursor++}return!1}return!0}function _(){var e,r,s=v.cursor;if(v.in_grouping(d,97,250))if(e=v.cursor,p()){if(v.cursor=e,function(){if(v.in_grouping(d,97,250))for(;!v.out_grouping(d,97,250);){if(v.cursor>=v.limit)return!1;v.cursor++}return i=v.cursor,!0}())return}else i=v.cursor;if(v.cursor=s,v.out_grouping(d,97,250)){if(r=v.cursor,p()){if(v.cursor=r,!v.in_grouping(d,97,250)||v.cursor>=v.limit)return;v.cursor++}i=v.cursor}}function h(){for(;!v.in_grouping(d,97,250);){if(v.cursor>=v.limit)return!1;v.cursor++}for(;!v.out_grouping(d,97,250);){if(v.cursor>=v.limit)return!1;v.cursor++}return!0}function b(){return i<=v.cursor}function g(){return e<=v.cursor}function k(){var e;if(v.ket=v.cursor,!(e=v.find_among_b(m,45)))return!1;switch(v.bra=v.cursor,e){case 1:if(!g())return!1;v.slice_del();break;case 2:if(!g())return!1;v.slice_from("log");break;case 3:if(!g())return!1;v.slice_from("u");break;case 4:if(!g())return!1;v.slice_from("ente");break;case 5:if(!(n<=v.cursor))return!1;v.slice_del(),v.ket=v.cursor,(e=v.find_among_b(t,4))&&(v.bra=v.cursor,g()&&(v.slice_del(),1==e&&(v.ket=v.cursor,v.eq_s_b(2,"at")&&(v.bra=v.cursor,g()&&v.slice_del()))));break;case 6:if(!g())return!1;v.slice_del(),v.ket=v.cursor,(e=v.find_among_b(u,3))&&(v.bra=v.cursor,1==e&&g()&&v.slice_del());break;case 7:if(!g())return!1;v.slice_del(),v.ket=v.cursor,(e=v.find_among_b(w,3))&&(v.bra=v.cursor,1==e&&g()&&v.slice_del());break;case 8:if(!g())return!1;v.slice_del(),v.ket=v.cursor,v.eq_s_b(2,"at")&&(v.bra=v.cursor,g()&&v.slice_del());break;case 9:if(!b()||!v.eq_s_b(1,"e"))return!1;v.slice_from("ir")}return!0}function q(e,r){if(v.eq_s_b(1,e)){v.bra=v.cursor;var s=v.limit-v.cursor;if(v.eq_s_b(1,r))return v.cursor=v.limit-s,b()&&v.slice_del(),!1}return!0}function j(){if(!k()&&(v.cursor=v.limit,!function(){var e,r;if(v.cursor>=i){if(r=v.limit_backward,v.limit_backward=i,v.ket=v.cursor,e=v.find_among_b(c,120))return v.bra=v.cursor,1==e&&v.slice_del(),v.limit_backward=r,!0;v.limit_backward=r}return!1}()))return v.cursor=v.limit,v.ket=v.cursor,void((e=v.find_among_b(l,7))&&(v.bra=v.cursor,1==e&&b()&&v.slice_del()));var e;v.cursor=v.limit,v.ket=v.cursor,v.eq_s_b(1,"i")&&(v.bra=v.cursor,v.eq_s_b(1,"c")&&(v.cursor=v.limit,b()&&v.slice_del()))}this.setCurrent=function(e){v.setCurrent(e)},this.getCurrent=function(){return v.getCurrent()},this.stem=function(){var r,s=v.cursor;return function(){for(var e;;){if(v.bra=v.cursor,e=v.find_among(o,3))switch(v.ket=v.cursor,e){case 1:v.slice_from("a~");continue;case 2:v.slice_from("o~");continue;case 3:if(v.cursor>=v.limit)break;v.cursor++;continue}break}}(),v.cursor=s,r=v.cursor,i=v.limit,n=i,e=i,_(),v.cursor=r,h()&&(n=v.cursor,h()&&(e=v.cursor)),v.limit_backward=s,v.cursor=v.limit,j(),v.cursor=v.limit,function(){var e;if(v.ket=v.cursor,e=v.find_among_b(f,4))switch(v.bra=v.cursor,e){case 1:b()&&(v.slice_del(),v.ket=v.cursor,v.limit,v.cursor,q("u","g")&&q("i","c"));break;case 2:v.slice_from("c")}}(),v.cursor=v.limit_backward,function(){for(var e;;){if(v.bra=v.cursor,e=v.find_among(a,3))switch(v.ket=v.cursor,e){case 1:v.slice_from("ã");continue;case 2:v.slice_from("õ");continue;case 3:if(v.cursor>=v.limit)break;v.cursor++;continue}break}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}),e.Pipeline.registerFunction(e.pt.stemmer,"stemmer-pt"),e.pt.stopWordFilter=e.generateStopWordFilter("a ao aos aquela aquelas aquele aqueles aquilo as até com como da das de dela delas dele deles depois do dos e ela elas ele eles em entre era eram essa essas esse esses esta estamos estas estava estavam este esteja estejam estejamos estes esteve estive estivemos estiver estivera estiveram estiverem estivermos estivesse estivessem estivéramos estivéssemos estou está estávamos estão eu foi fomos for fora foram forem formos fosse fossem fui fôramos fôssemos haja hajam hajamos havemos hei houve houvemos houver houvera houveram houverei houverem houveremos houveria houveriam houvermos houverá houverão houveríamos houvesse houvessem houvéramos houvéssemos há hão isso isto já lhe lhes mais mas me mesmo meu meus minha minhas muito na nas nem no nos nossa nossas nosso nossos num numa não nós o os ou para pela pelas pelo pelos por qual quando que quem se seja sejam sejamos sem serei seremos seria seriam será serão seríamos seu seus somos sou sua suas são só também te tem temos tenha tenham tenhamos tenho terei teremos teria teriam terá terão teríamos teu teus teve tinha tinham tive tivemos tiver tivera tiveram tiverem tivermos tivesse tivessem tivéramos tivéssemos tu tua tuas tém tínhamos um uma você vocês vos à às éramos".split(" ")),e.Pipeline.registerFunction(e.pt.stopWordFilter,"stopWordFilter-pt")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.ro.js b/v1/assets/javascripts/lunr/lunr.ro.js new file mode 100644 index 000000000..155cb5621 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.ro.js @@ -0,0 +1 @@ +!function(e,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i,r,n;e.ro=function(){this.pipeline.reset(),this.pipeline.add(e.ro.trimmer,e.ro.stopWordFilter,e.ro.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ro.stemmer))},e.ro.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.ro.trimmer=e.trimmerSupport.generateTrimmer(e.ro.wordCharacters),e.Pipeline.registerFunction(e.ro.trimmer,"trimmer-ro"),e.ro.stemmer=(i=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){var e,n,t,a,o=[new i("",-1,3),new i("I",0,1),new i("U",0,2)],s=[new i("ea",-1,3),new i("aţia",-1,7),new i("aua",-1,2),new i("iua",-1,4),new i("aţie",-1,7),new i("ele",-1,3),new i("ile",-1,5),new i("iile",6,4),new i("iei",-1,4),new i("atei",-1,6),new i("ii",-1,4),new i("ului",-1,1),new i("ul",-1,1),new i("elor",-1,3),new i("ilor",-1,4),new i("iilor",14,4)],c=[new i("icala",-1,4),new i("iciva",-1,4),new i("ativa",-1,5),new i("itiva",-1,6),new i("icale",-1,4),new i("aţiune",-1,5),new i("iţiune",-1,6),new i("atoare",-1,5),new i("itoare",-1,6),new i("ătoare",-1,5),new i("icitate",-1,4),new i("abilitate",-1,1),new i("ibilitate",-1,2),new i("ivitate",-1,3),new i("icive",-1,4),new i("ative",-1,5),new i("itive",-1,6),new i("icali",-1,4),new i("atori",-1,5),new i("icatori",18,4),new i("itori",-1,6),new i("ători",-1,5),new i("icitati",-1,4),new i("abilitati",-1,1),new i("ivitati",-1,3),new i("icivi",-1,4),new i("ativi",-1,5),new i("itivi",-1,6),new i("icităi",-1,4),new i("abilităi",-1,1),new i("ivităi",-1,3),new i("icităţi",-1,4),new i("abilităţi",-1,1),new i("ivităţi",-1,3),new i("ical",-1,4),new i("ator",-1,5),new i("icator",35,4),new i("itor",-1,6),new i("ător",-1,5),new i("iciv",-1,4),new i("ativ",-1,5),new i("itiv",-1,6),new i("icală",-1,4),new i("icivă",-1,4),new i("ativă",-1,5),new i("itivă",-1,6)],u=[new i("ica",-1,1),new i("abila",-1,1),new i("ibila",-1,1),new i("oasa",-1,1),new i("ata",-1,1),new i("ita",-1,1),new i("anta",-1,1),new i("ista",-1,3),new i("uta",-1,1),new i("iva",-1,1),new i("ic",-1,1),new i("ice",-1,1),new i("abile",-1,1),new i("ibile",-1,1),new i("isme",-1,3),new i("iune",-1,2),new i("oase",-1,1),new i("ate",-1,1),new i("itate",17,1),new i("ite",-1,1),new i("ante",-1,1),new i("iste",-1,3),new i("ute",-1,1),new i("ive",-1,1),new i("ici",-1,1),new i("abili",-1,1),new i("ibili",-1,1),new i("iuni",-1,2),new i("atori",-1,1),new i("osi",-1,1),new i("ati",-1,1),new i("itati",30,1),new i("iti",-1,1),new i("anti",-1,1),new i("isti",-1,3),new i("uti",-1,1),new i("işti",-1,3),new i("ivi",-1,1),new i("ităi",-1,1),new i("oşi",-1,1),new i("ităţi",-1,1),new i("abil",-1,1),new i("ibil",-1,1),new i("ism",-1,3),new i("ator",-1,1),new i("os",-1,1),new i("at",-1,1),new i("it",-1,1),new i("ant",-1,1),new i("ist",-1,3),new i("ut",-1,1),new i("iv",-1,1),new i("ică",-1,1),new i("abilă",-1,1),new i("ibilă",-1,1),new i("oasă",-1,1),new i("ată",-1,1),new i("ită",-1,1),new i("antă",-1,1),new i("istă",-1,3),new i("ută",-1,1),new i("ivă",-1,1)],w=[new i("ea",-1,1),new i("ia",-1,1),new i("esc",-1,1),new i("ăsc",-1,1),new i("ind",-1,1),new i("ând",-1,1),new i("are",-1,1),new i("ere",-1,1),new i("ire",-1,1),new i("âre",-1,1),new i("se",-1,2),new i("ase",10,1),new i("sese",10,2),new i("ise",10,1),new i("use",10,1),new i("âse",10,1),new i("eşte",-1,1),new i("ăşte",-1,1),new i("eze",-1,1),new i("ai",-1,1),new i("eai",19,1),new i("iai",19,1),new i("sei",-1,2),new i("eşti",-1,1),new i("ăşti",-1,1),new i("ui",-1,1),new i("ezi",-1,1),new i("âi",-1,1),new i("aşi",-1,1),new i("seşi",-1,2),new i("aseşi",29,1),new i("seseşi",29,2),new i("iseşi",29,1),new i("useşi",29,1),new i("âseşi",29,1),new i("işi",-1,1),new i("uşi",-1,1),new i("âşi",-1,1),new i("aţi",-1,2),new i("eaţi",38,1),new i("iaţi",38,1),new i("eţi",-1,2),new i("iţi",-1,2),new i("âţi",-1,2),new i("arăţi",-1,1),new i("serăţi",-1,2),new i("aserăţi",45,1),new i("seserăţi",45,2),new i("iserăţi",45,1),new i("userăţi",45,1),new i("âserăţi",45,1),new i("irăţi",-1,1),new i("urăţi",-1,1),new i("ârăţi",-1,1),new i("am",-1,1),new i("eam",54,1),new i("iam",54,1),new i("em",-1,2),new i("asem",57,1),new i("sesem",57,2),new i("isem",57,1),new i("usem",57,1),new i("âsem",57,1),new i("im",-1,2),new i("âm",-1,2),new i("ăm",-1,2),new i("arăm",65,1),new i("serăm",65,2),new i("aserăm",67,1),new i("seserăm",67,2),new i("iserăm",67,1),new i("userăm",67,1),new i("âserăm",67,1),new i("irăm",65,1),new i("urăm",65,1),new i("ârăm",65,1),new i("au",-1,1),new i("eau",76,1),new i("iau",76,1),new i("indu",-1,1),new i("ându",-1,1),new i("ez",-1,1),new i("ească",-1,1),new i("ară",-1,1),new i("seră",-1,2),new i("aseră",84,1),new i("seseră",84,2),new i("iseră",84,1),new i("useră",84,1),new i("âseră",84,1),new i("iră",-1,1),new i("ură",-1,1),new i("âră",-1,1),new i("ează",-1,1)],m=[new i("a",-1,1),new i("e",-1,1),new i("ie",1,1),new i("i",-1,1),new i("ă",-1,1)],l=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,2,32,0,0,4],f=new r;function p(e,i){f.eq_s(1,e)&&(f.ket=f.cursor,f.in_grouping(l,97,259)&&f.slice_from(i))}function d(){if(f.out_grouping(l,97,259)){for(;!f.in_grouping(l,97,259);){if(f.cursor>=f.limit)return!0;f.cursor++}return!1}return!0}function b(){var e,i,r=f.cursor;if(f.in_grouping(l,97,259)){if(e=f.cursor,!d())return void(a=f.cursor);if(f.cursor=e,!function(){if(f.in_grouping(l,97,259))for(;!f.out_grouping(l,97,259);){if(f.cursor>=f.limit)return!0;f.cursor++}return!1}())return void(a=f.cursor)}f.cursor=r,f.out_grouping(l,97,259)&&(i=f.cursor,d()&&(f.cursor=i,f.in_grouping(l,97,259)&&f.cursor=f.limit)return!1;f.cursor++}for(;!f.out_grouping(l,97,259);){if(f.cursor>=f.limit)return!1;f.cursor++}return!0}function _(){return t<=f.cursor}function g(){var i,r=f.limit-f.cursor;if(f.ket=f.cursor,(i=f.find_among_b(c,46))&&(f.bra=f.cursor,_())){switch(i){case 1:f.slice_from("abil");break;case 2:f.slice_from("ibil");break;case 3:f.slice_from("iv");break;case 4:f.slice_from("ic");break;case 5:f.slice_from("at");break;case 6:f.slice_from("it")}return e=!0,f.cursor=f.limit-r,!0}return!1}function k(){var i,r;for(e=!1;;)if(r=f.limit-f.cursor,!g()){f.cursor=f.limit-r;break}if(f.ket=f.cursor,(i=f.find_among_b(u,62))&&(f.bra=f.cursor,n<=f.cursor)){switch(i){case 1:f.slice_del();break;case 2:f.eq_s_b(1,"ţ")&&(f.bra=f.cursor,f.slice_from("t"));break;case 3:f.slice_from("ist")}e=!0}}function h(){var e;f.ket=f.cursor,(e=f.find_among_b(m,5))&&(f.bra=f.cursor,a<=f.cursor&&1==e&&f.slice_del())}this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var i,r=f.cursor;return function(){for(var e,i;e=f.cursor,f.in_grouping(l,97,259)&&(i=f.cursor,f.bra=i,p("u","U"),f.cursor=i,p("i","I")),f.cursor=e,!(f.cursor>=f.limit);)f.cursor++}(),f.cursor=r,i=f.cursor,a=f.limit,t=a,n=a,b(),f.cursor=i,v()&&(t=f.cursor,v()&&(n=f.cursor)),f.limit_backward=r,f.cursor=f.limit,function(){var e,i;if(f.ket=f.cursor,(e=f.find_among_b(s,16))&&(f.bra=f.cursor,_()))switch(e){case 1:f.slice_del();break;case 2:f.slice_from("a");break;case 3:f.slice_from("e");break;case 4:f.slice_from("i");break;case 5:i=f.limit-f.cursor,f.eq_s_b(2,"ab")||(f.cursor=f.limit-i,f.slice_from("i"));break;case 6:f.slice_from("at");break;case 7:f.slice_from("aţi")}}(),f.cursor=f.limit,k(),f.cursor=f.limit,e||(f.cursor=f.limit,function(){var e,i,r;if(f.cursor>=a){if(i=f.limit_backward,f.limit_backward=a,f.ket=f.cursor,e=f.find_among_b(w,94))switch(f.bra=f.cursor,e){case 1:if(r=f.limit-f.cursor,!f.out_grouping_b(l,97,259)&&(f.cursor=f.limit-r,!f.eq_s_b(1,"u")))break;case 2:f.slice_del()}f.limit_backward=i}}(),f.cursor=f.limit),h(),f.cursor=f.limit_backward,function(){for(var e;;){if(f.bra=f.cursor,e=f.find_among(o,3))switch(f.ket=f.cursor,e){case 1:f.slice_from("i");continue;case 2:f.slice_from("u");continue;case 3:if(f.cursor>=f.limit)break;f.cursor++;continue}break}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}),e.Pipeline.registerFunction(e.ro.stemmer,"stemmer-ro"),e.ro.stopWordFilter=e.generateStopWordFilter("acea aceasta această aceea acei aceia acel acela acele acelea acest acesta aceste acestea aceşti aceştia acolo acord acum ai aia aibă aici al ale alea altceva altcineva am ar are asemenea asta astea astăzi asupra au avea avem aveţi azi aş aşadar aţi bine bucur bună ca care caut ce cel ceva chiar cinci cine cineva contra cu cum cumva curând curînd când cât câte câtva câţi cînd cît cîte cîtva cîţi că căci cărei căror cărui către da dacă dar datorită dată dau de deci deja deoarece departe deşi din dinaintea dintr- dintre doi doilea două drept după dă ea ei el ele eram este eu eşti face fata fi fie fiecare fii fim fiu fiţi frumos fără graţie halbă iar ieri la le li lor lui lângă lîngă mai mea mei mele mereu meu mi mie mine mult multă mulţi mulţumesc mâine mîine mă ne nevoie nici nicăieri nimeni nimeri nimic nişte noastre noastră noi noroc nostru nouă noştri nu opt ori oricare orice oricine oricum oricând oricât oricînd oricît oriunde patra patru patrulea pe pentru peste pic poate pot prea prima primul prin puţin puţina puţină până pînă rog sa sale sau se spate spre sub sunt suntem sunteţi sută sînt sîntem sînteţi să săi său ta tale te timp tine toate toată tot totuşi toţi trei treia treilea tu tăi tău un una unde undeva unei uneia unele uneori unii unor unora unu unui unuia unul vi voastre voastră voi vostru vouă voştri vreme vreo vreun vă zece zero zi zice îi îl îmi împotriva în înainte înaintea încotro încât încît între întrucât întrucît îţi ăla ălea ăsta ăstea ăştia şapte şase şi ştiu ţi ţie".split(" ")),e.Pipeline.registerFunction(e.ro.stopWordFilter,"stopWordFilter-ro")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.ru.js b/v1/assets/javascripts/lunr/lunr.ru.js new file mode 100644 index 000000000..078609ad8 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.ru.js @@ -0,0 +1 @@ +!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var n,r,t;e.ru=function(){this.pipeline.reset(),this.pipeline.add(e.ru.trimmer,e.ru.stopWordFilter,e.ru.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ru.stemmer))},e.ru.wordCharacters="Ѐ-҄҇-ԯᴫᵸⷠ-ⷿꙀ-ꚟ︮︯",e.ru.trimmer=e.trimmerSupport.generateTrimmer(e.ru.wordCharacters),e.Pipeline.registerFunction(e.ru.trimmer,"trimmer-ru"),e.ru.stemmer=(n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,t=new function(){var e,t,w=[new n("в",-1,1),new n("ив",0,2),new n("ыв",0,2),new n("вши",-1,1),new n("ивши",3,2),new n("ывши",3,2),new n("вшись",-1,1),new n("ившись",6,2),new n("ывшись",6,2)],i=[new n("ее",-1,1),new n("ие",-1,1),new n("ое",-1,1),new n("ые",-1,1),new n("ими",-1,1),new n("ыми",-1,1),new n("ей",-1,1),new n("ий",-1,1),new n("ой",-1,1),new n("ый",-1,1),new n("ем",-1,1),new n("им",-1,1),new n("ом",-1,1),new n("ым",-1,1),new n("его",-1,1),new n("ого",-1,1),new n("ему",-1,1),new n("ому",-1,1),new n("их",-1,1),new n("ых",-1,1),new n("ею",-1,1),new n("ою",-1,1),new n("ую",-1,1),new n("юю",-1,1),new n("ая",-1,1),new n("яя",-1,1)],u=[new n("ем",-1,1),new n("нн",-1,1),new n("вш",-1,1),new n("ивш",2,2),new n("ывш",2,2),new n("щ",-1,1),new n("ющ",5,1),new n("ующ",6,2)],s=[new n("сь",-1,1),new n("ся",-1,1)],o=[new n("ла",-1,1),new n("ила",0,2),new n("ыла",0,2),new n("на",-1,1),new n("ена",3,2),new n("ете",-1,1),new n("ите",-1,2),new n("йте",-1,1),new n("ейте",7,2),new n("уйте",7,2),new n("ли",-1,1),new n("или",10,2),new n("ыли",10,2),new n("й",-1,1),new n("ей",13,2),new n("уй",13,2),new n("л",-1,1),new n("ил",16,2),new n("ыл",16,2),new n("ем",-1,1),new n("им",-1,2),new n("ым",-1,2),new n("н",-1,1),new n("ен",22,2),new n("ло",-1,1),new n("ило",24,2),new n("ыло",24,2),new n("но",-1,1),new n("ено",27,2),new n("нно",27,1),new n("ет",-1,1),new n("ует",30,2),new n("ит",-1,2),new n("ыт",-1,2),new n("ют",-1,1),new n("уют",34,2),new n("ят",-1,2),new n("ны",-1,1),new n("ены",37,2),new n("ть",-1,1),new n("ить",39,2),new n("ыть",39,2),new n("ешь",-1,1),new n("ишь",-1,2),new n("ю",-1,2),new n("ую",44,2)],c=[new n("а",-1,1),new n("ев",-1,1),new n("ов",-1,1),new n("е",-1,1),new n("ие",3,1),new n("ье",3,1),new n("и",-1,1),new n("еи",6,1),new n("ии",6,1),new n("ами",6,1),new n("ями",6,1),new n("иями",10,1),new n("й",-1,1),new n("ей",12,1),new n("ией",13,1),new n("ий",12,1),new n("ой",12,1),new n("ам",-1,1),new n("ем",-1,1),new n("ием",18,1),new n("ом",-1,1),new n("ям",-1,1),new n("иям",21,1),new n("о",-1,1),new n("у",-1,1),new n("ах",-1,1),new n("ях",-1,1),new n("иях",26,1),new n("ы",-1,1),new n("ь",-1,1),new n("ю",-1,1),new n("ию",30,1),new n("ью",30,1),new n("я",-1,1),new n("ия",33,1),new n("ья",33,1)],m=[new n("ост",-1,1),new n("ость",-1,1)],l=[new n("ейше",-1,1),new n("н",-1,2),new n("ейш",-1,1),new n("ь",-1,3)],f=[33,65,8,232],a=new r;function p(){for(;!a.in_grouping(f,1072,1103);){if(a.cursor>=a.limit)return!1;a.cursor++}return!0}function d(){for(;!a.out_grouping(f,1072,1103);){if(a.cursor>=a.limit)return!1;a.cursor++}return!0}function _(e,n){var r,t;if(a.ket=a.cursor,r=a.find_among_b(e,n)){switch(a.bra=a.cursor,r){case 1:if(t=a.limit-a.cursor,!a.eq_s_b(1,"а")&&(a.cursor=a.limit-t,!a.eq_s_b(1,"я")))return!1;case 2:a.slice_del()}return!0}return!1}function b(e,n){var r;return a.ket=a.cursor,!!(r=a.find_among_b(e,n))&&(a.bra=a.cursor,1==r&&a.slice_del(),!0)}function h(){return!!b(i,26)&&(_(u,8),!0)}function g(){var n;a.ket=a.cursor,(n=a.find_among_b(m,2))&&(a.bra=a.cursor,e<=a.cursor&&1==n&&a.slice_del())}this.setCurrent=function(e){a.setCurrent(e)},this.getCurrent=function(){return a.getCurrent()},this.stem=function(){return t=a.limit,e=t,p()&&(t=a.cursor,d()&&p()&&d()&&(e=a.cursor)),a.cursor=a.limit,!(a.cursor=i&&t[(e-=i)>>3]&1<<(7&e))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&t[(e-=i)>>3]&1<<(7&e))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursors||e>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor>1),f=0,l=o0||e==s||c)break;c=!0}}for(;;){if(o>=(_=t[s]).s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o=0;_--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-m.s[_])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var m;if(o>=(m=t[s]).s_size){if(this.cursor=n-m.s_size,!m.method)return m.result;var b=m.method();if(this.cursor=n-m.s_size,b)return m.result}if((s=m.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.sv.js b/v1/assets/javascripts/lunr/lunr.sv.js new file mode 100644 index 000000000..4bb0f9f92 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.sv.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r,n,t;e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=(r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){var e,t,i=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],s=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],a=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],o=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],u=[119,127,149],m=new n;this.setCurrent=function(e){m.setCurrent(e)},this.getCurrent=function(){return m.getCurrent()},this.stem=function(){var r,n=m.cursor;return function(){var r,n=m.cursor+3;if(t=m.limit,0<=n||n<=m.limit){for(e=n;;){if(r=m.cursor,m.in_grouping(o,97,246)){m.cursor=r;break}if(m.cursor=r,m.cursor>=m.limit)return;m.cursor++}for(;!m.out_grouping(o,97,246);){if(m.cursor>=m.limit)return;m.cursor++}(t=m.cursor)=t&&(m.limit_backward=t,m.cursor=m.limit,m.ket=m.cursor,e=m.find_among_b(i,37),m.limit_backward=r,e))switch(m.bra=m.cursor,e){case 1:m.slice_del();break;case 2:m.in_grouping_b(u,98,121)&&m.slice_del()}}(),m.cursor=m.limit,r=m.limit_backward,m.cursor>=t&&(m.limit_backward=t,m.cursor=m.limit,m.find_among_b(s,7)&&(m.cursor=m.limit,m.ket=m.cursor,m.cursor>m.limit_backward&&(m.bra=--m.cursor,m.slice_del())),m.limit_backward=r),m.cursor=m.limit,function(){var e,r;if(m.cursor>=t){if(r=m.limit_backward,m.limit_backward=t,m.cursor=m.limit,m.ket=m.cursor,e=m.find_among_b(a,5))switch(m.bra=m.cursor,e){case 1:m.slice_del();break;case 2:m.slice_from("lös");break;case 3:m.slice_from("full")}m.limit_backward=r}}(),!0}},function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/lunr.tr.js b/v1/assets/javascripts/lunr/lunr.tr.js new file mode 100644 index 000000000..c42b349e8 --- /dev/null +++ b/v1/assets/javascripts/lunr/lunr.tr.js @@ -0,0 +1 @@ +!function(r,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(r.lunr)}(this,function(){return function(r){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i,e,n;r.tr=function(){this.pipeline.reset(),this.pipeline.add(r.tr.trimmer,r.tr.stopWordFilter,r.tr.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(r.tr.stemmer))},r.tr.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",r.tr.trimmer=r.trimmerSupport.generateTrimmer(r.tr.wordCharacters),r.Pipeline.registerFunction(r.tr.trimmer,"trimmer-tr"),r.tr.stemmer=(i=r.stemmerSupport.Among,e=r.stemmerSupport.SnowballProgram,n=new function(){var r,n=[new i("m",-1,-1),new i("n",-1,-1),new i("miz",-1,-1),new i("niz",-1,-1),new i("muz",-1,-1),new i("nuz",-1,-1),new i("müz",-1,-1),new i("nüz",-1,-1),new i("mız",-1,-1),new i("nız",-1,-1)],t=[new i("leri",-1,-1),new i("ları",-1,-1)],u=[new i("ni",-1,-1),new i("nu",-1,-1),new i("nü",-1,-1),new i("nı",-1,-1)],o=[new i("in",-1,-1),new i("un",-1,-1),new i("ün",-1,-1),new i("ın",-1,-1)],s=[new i("a",-1,-1),new i("e",-1,-1)],c=[new i("na",-1,-1),new i("ne",-1,-1)],l=[new i("da",-1,-1),new i("ta",-1,-1),new i("de",-1,-1),new i("te",-1,-1)],a=[new i("nda",-1,-1),new i("nde",-1,-1)],m=[new i("dan",-1,-1),new i("tan",-1,-1),new i("den",-1,-1),new i("ten",-1,-1)],d=[new i("ndan",-1,-1),new i("nden",-1,-1)],f=[new i("la",-1,-1),new i("le",-1,-1)],b=[new i("ca",-1,-1),new i("ce",-1,-1)],w=[new i("im",-1,-1),new i("um",-1,-1),new i("üm",-1,-1),new i("ım",-1,-1)],_=[new i("sin",-1,-1),new i("sun",-1,-1),new i("sün",-1,-1),new i("sın",-1,-1)],k=[new i("iz",-1,-1),new i("uz",-1,-1),new i("üz",-1,-1),new i("ız",-1,-1)],p=[new i("siniz",-1,-1),new i("sunuz",-1,-1),new i("sünüz",-1,-1),new i("sınız",-1,-1)],g=[new i("lar",-1,-1),new i("ler",-1,-1)],y=[new i("niz",-1,-1),new i("nuz",-1,-1),new i("nüz",-1,-1),new i("nız",-1,-1)],z=[new i("dir",-1,-1),new i("tir",-1,-1),new i("dur",-1,-1),new i("tur",-1,-1),new i("dür",-1,-1),new i("tür",-1,-1),new i("dır",-1,-1),new i("tır",-1,-1)],h=[new i("casına",-1,-1),new i("cesine",-1,-1)],v=[new i("di",-1,-1),new i("ti",-1,-1),new i("dik",-1,-1),new i("tik",-1,-1),new i("duk",-1,-1),new i("tuk",-1,-1),new i("dük",-1,-1),new i("tük",-1,-1),new i("dık",-1,-1),new i("tık",-1,-1),new i("dim",-1,-1),new i("tim",-1,-1),new i("dum",-1,-1),new i("tum",-1,-1),new i("düm",-1,-1),new i("tüm",-1,-1),new i("dım",-1,-1),new i("tım",-1,-1),new i("din",-1,-1),new i("tin",-1,-1),new i("dun",-1,-1),new i("tun",-1,-1),new i("dün",-1,-1),new i("tün",-1,-1),new i("dın",-1,-1),new i("tın",-1,-1),new i("du",-1,-1),new i("tu",-1,-1),new i("dü",-1,-1),new i("tü",-1,-1),new i("dı",-1,-1),new i("tı",-1,-1)],q=[new i("sa",-1,-1),new i("se",-1,-1),new i("sak",-1,-1),new i("sek",-1,-1),new i("sam",-1,-1),new i("sem",-1,-1),new i("san",-1,-1),new i("sen",-1,-1)],C=[new i("miş",-1,-1),new i("muş",-1,-1),new i("müş",-1,-1),new i("mış",-1,-1)],P=[new i("b",-1,1),new i("c",-1,2),new i("d",-1,3),new i("ğ",-1,4)],F=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,8,0,0,0,0,0,0,1],S=[1,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,1],W=[65],L=[65],x=[["a",[1,64,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],97,305],["e",[17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,130],101,252],["ı",[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],97,305],["i",[17],101,105],["o",W,111,117],["ö",L,246,252],["u",W,111,117]],A=new e;function E(r,i,e){for(;;){var n=A.limit-A.cursor;if(A.in_grouping_b(r,i,e)){A.cursor=A.limit-n;break}if(A.cursor=A.limit-n,A.cursor<=A.limit_backward)return!1;A.cursor--}return!0}function j(){var r,i;r=A.limit-A.cursor,E(F,97,305);for(var e=0;eA.limit_backward&&(A.cursor--,e=A.limit-A.cursor,i()))?(A.cursor=A.limit-e,!0):(A.cursor=A.limit-n,r()?(A.cursor=A.limit-n,!1):(A.cursor=A.limit-n,!(A.cursor<=A.limit_backward)&&(A.cursor--,!!i()&&(A.cursor=A.limit-n,!0))))}function Z(r){return T(r,function(){return A.in_grouping_b(F,97,305)})}function B(){return Z(function(){return A.eq_s_b(1,"n")})}function D(){return Z(function(){return A.eq_s_b(1,"y")})}function G(){return A.find_among_b(n,10)&&T(function(){return A.in_grouping_b(S,105,305)},function(){return A.out_grouping_b(F,97,305)})}function H(){return j()&&A.in_grouping_b(S,105,305)&&Z(function(){return A.eq_s_b(1,"s")})}function I(){return A.find_among_b(t,2)}function J(){return j()&&A.find_among_b(o,4)&&B()}function K(){return j()&&A.find_among_b(l,4)}function M(){return j()&&A.find_among_b(a,2)}function N(){return j()&&A.find_among_b(w,4)&&D()}function O(){return j()&&A.find_among_b(_,4)}function Q(){return j()&&A.find_among_b(k,4)&&D()}function R(){return A.find_among_b(p,4)}function U(){return j()&&A.find_among_b(g,2)}function V(){return j()&&A.find_among_b(z,8)}function X(){return j()&&A.find_among_b(v,32)&&D()}function Y(){return A.find_among_b(q,8)&&D()}function $(){return j()&&A.find_among_b(C,4)&&D()}function rr(){var r=A.limit-A.cursor;return!($()||(A.cursor=A.limit-r,X()||(A.cursor=A.limit-r,Y()||(A.cursor=A.limit-r,A.eq_s_b(3,"ken")&&D()))))}function ir(){if(A.find_among_b(h,2)){var r=A.limit-A.cursor;if(R()||(A.cursor=A.limit-r,U()||(A.cursor=A.limit-r,N()||(A.cursor=A.limit-r,O()||(A.cursor=A.limit-r,Q()||(A.cursor=A.limit-r))))),$())return!1}return!0}function er(){if(!j()||!A.find_among_b(y,4))return!0;var r=A.limit-A.cursor;return!X()&&(A.cursor=A.limit-r,!Y())}function nr(){var i,e,n,t=A.limit-A.cursor;if(A.ket=A.cursor,r=!0,rr()&&(A.cursor=A.limit-t,ir()&&(A.cursor=A.limit-t,function(){if(U()){A.bra=A.cursor,A.slice_del();var i=A.limit-A.cursor;return A.ket=A.cursor,V()||(A.cursor=A.limit-i,X()||(A.cursor=A.limit-i,Y()||(A.cursor=A.limit-i,$()||(A.cursor=A.limit-i)))),r=!1,!1}return!0}()&&(A.cursor=A.limit-t,er()&&(A.cursor=A.limit-t,n=A.limit-A.cursor,!(R()||(A.cursor=A.limit-n,Q()||(A.cursor=A.limit-n,O()||(A.cursor=A.limit-n,N()))))||(A.bra=A.cursor,A.slice_del(),e=A.limit-A.cursor,A.ket=A.cursor,$()||(A.cursor=A.limit-e),0)))))){if(A.cursor=A.limit-t,!V())return;A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,i=A.limit-A.cursor,R()||(A.cursor=A.limit-i,U()||(A.cursor=A.limit-i,N()||(A.cursor=A.limit-i,O()||(A.cursor=A.limit-i,Q()||(A.cursor=A.limit-i))))),$()||(A.cursor=A.limit-i)}A.bra=A.cursor,A.slice_del()}function tr(){var r,i,e,n;if(A.ket=A.cursor,A.eq_s_b(2,"ki")){if(r=A.limit-A.cursor,K())return A.bra=A.cursor,A.slice_del(),i=A.limit-A.cursor,A.ket=A.cursor,U()?(A.bra=A.cursor,A.slice_del(),tr()):(A.cursor=A.limit-i,G()&&(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr()))),!0;if(A.cursor=A.limit-r,J()){if(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,e=A.limit-A.cursor,I())A.bra=A.cursor,A.slice_del();else{if(A.cursor=A.limit-e,A.ket=A.cursor,!G()&&(A.cursor=A.limit-e,!H()&&(A.cursor=A.limit-e,!tr())))return!0;A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr())}return!0}if(A.cursor=A.limit-r,M()){if(n=A.limit-A.cursor,I())A.bra=A.cursor,A.slice_del();else if(A.cursor=A.limit-n,H())A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr());else if(A.cursor=A.limit-n,!tr())return!1;return!0}}return!1}function ur(r){if(A.ket=A.cursor,!M()&&(A.cursor=A.limit-r,!j()||!A.find_among_b(c,2)))return!1;var i=A.limit-A.cursor;if(I())A.bra=A.cursor,A.slice_del();else if(A.cursor=A.limit-i,H())A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr());else if(A.cursor=A.limit-i,!tr())return!1;return!0}function or(r){if(A.ket=A.cursor,!(j()&&A.find_among_b(d,2)||(A.cursor=A.limit-r,j()&&A.find_among_b(u,4))))return!1;var i=A.limit-A.cursor;return!(!H()&&(A.cursor=A.limit-i,!I()))&&(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr()),!0)}function sr(){var r,i=A.limit-A.cursor;return A.ket=A.cursor,!!(J()||(A.cursor=A.limit-i,j()&&A.find_among_b(f,2)&&D()))&&(A.bra=A.cursor,A.slice_del(),r=A.limit-A.cursor,A.ket=A.cursor,!(!U()||(A.bra=A.cursor,A.slice_del(),!tr()))||(A.cursor=A.limit-r,A.ket=A.cursor,!(G()||(A.cursor=A.limit-r,H()||(A.cursor=A.limit-r,tr())))||(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr()),!0)))}function cr(){var r,i,e=A.limit-A.cursor;if(A.ket=A.cursor,!(K()||(A.cursor=A.limit-e,j()&&A.in_grouping_b(S,105,305)&&D()||(A.cursor=A.limit-e,j()&&A.find_among_b(s,2)&&D()))))return!1;if(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,r=A.limit-A.cursor,G())A.bra=A.cursor,A.slice_del(),i=A.limit-A.cursor,A.ket=A.cursor,U()||(A.cursor=A.limit-i);else if(A.cursor=A.limit-r,!U())return!0;return A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,tr(),!0}function lr(){var r,i,e=A.limit-A.cursor;if(A.ket=A.cursor,U())return A.bra=A.cursor,A.slice_del(),void tr();if(A.cursor=A.limit-e,A.ket=A.cursor,j()&&A.find_among_b(b,2)&&B())if(A.bra=A.cursor,A.slice_del(),r=A.limit-A.cursor,A.ket=A.cursor,I())A.bra=A.cursor,A.slice_del();else{if(A.cursor=A.limit-r,A.ket=A.cursor,!G()&&(A.cursor=A.limit-r,!H())){if(A.cursor=A.limit-r,A.ket=A.cursor,!U())return;if(A.bra=A.cursor,A.slice_del(),!tr())return}A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr())}else if(A.cursor=A.limit-e,!ur(e)&&(A.cursor=A.limit-e,!or(e))){if(A.cursor=A.limit-e,A.ket=A.cursor,j()&&A.find_among_b(m,4))return A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,i=A.limit-A.cursor,void(G()?(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr())):(A.cursor=A.limit-i,U()?(A.bra=A.cursor,A.slice_del(),tr()):(A.cursor=A.limit-i,tr())));if(A.cursor=A.limit-e,!sr()){if(A.cursor=A.limit-e,I())return A.bra=A.cursor,void A.slice_del();A.cursor=A.limit-e,tr()||(A.cursor=A.limit-e,cr()||(A.cursor=A.limit-e,A.ket=A.cursor,(G()||(A.cursor=A.limit-e,H()))&&(A.bra=A.cursor,A.slice_del(),A.ket=A.cursor,U()&&(A.bra=A.cursor,A.slice_del(),tr()))))}}}function ar(r,i,e){if(A.cursor=A.limit-r,function(){for(;;){var r=A.limit-A.cursor;if(A.in_grouping_b(F,97,305)){A.cursor=A.limit-r;break}if(A.cursor=A.limit-r,A.cursor<=A.limit_backward)return!1;A.cursor--}return!0}()){var n=A.limit-A.cursor;if(!A.eq_s_b(1,i)&&(A.cursor=A.limit-n,!A.eq_s_b(1,e)))return!0;A.cursor=A.limit-r;var t=A.cursor;return A.insert(A.cursor,A.cursor,e),A.cursor=t,!1}return!0}function mr(r,i,e){for(;!A.eq_s(i,e);){if(A.cursor>=A.limit)return!0;A.cursor++}return i!=A.limit||(A.cursor=r,!1)}function dr(){var r,i,e=A.cursor;return!(!mr(r=A.cursor,2,"ad")||(A.cursor=r,!mr(r,5,"soyad")))&&(A.limit_backward=e,A.cursor=A.limit,i=A.limit-A.cursor,(A.eq_s_b(1,"d")||(A.cursor=A.limit-i,A.eq_s_b(1,"g")))&&ar(i,"a","ı")&&ar(i,"e","i")&&ar(i,"o","u")&&ar(i,"ö","ü"),A.cursor=A.limit,function(){var r;if(A.ket=A.cursor,r=A.find_among_b(P,4))switch(A.bra=A.cursor,r){case 1:A.slice_from("p");break;case 2:A.slice_from("ç");break;case 3:A.slice_from("t");break;case 4:A.slice_from("k")}}(),!0)}this.setCurrent=function(r){A.setCurrent(r)},this.getCurrent=function(){return A.getCurrent()},this.stem=function(){return!!(function(){for(var r,i=A.cursor,e=2;;){for(r=A.cursor;!A.in_grouping(F,97,305);){if(A.cursor>=A.limit)return A.cursor=r,!(e>0||(A.cursor=i,0));A.cursor++}e--}}()&&(A.limit_backward=A.cursor,A.cursor=A.limit,nr(),A.cursor=A.limit,r&&(lr(),A.cursor=A.limit_backward,dr())))}},function(r){return"function"==typeof r.update?r.update(function(r){return n.setCurrent(r),n.stem(),n.getCurrent()}):(n.setCurrent(r),n.stem(),n.getCurrent())}),r.Pipeline.registerFunction(r.tr.stemmer,"stemmer-tr"),r.tr.stopWordFilter=r.generateStopWordFilter("acaba altmış altı ama ancak arada aslında ayrıca bana bazı belki ben benden beni benim beri beş bile bin bir biri birkaç birkez birçok birşey birşeyi biz bizden bize bizi bizim bu buna bunda bundan bunlar bunları bunların bunu bunun burada böyle böylece da daha dahi de defa değil diye diğer doksan dokuz dolayı dolayısıyla dört edecek eden ederek edilecek ediliyor edilmesi ediyor elli en etmesi etti ettiği ettiğini eğer gibi göre halen hangi hatta hem henüz hep hepsi her herhangi herkesin hiç hiçbir iki ile ilgili ise itibaren itibariyle için işte kadar karşın katrilyon kendi kendilerine kendini kendisi kendisine kendisini kez ki kim kimden kime kimi kimse kırk milyar milyon mu mü mı nasıl ne neden nedenle nerde nerede nereye niye niçin o olan olarak oldu olduklarını olduğu olduğunu olmadı olmadığı olmak olması olmayan olmaz olsa olsun olup olur olursa oluyor on ona ondan onlar onlardan onları onların onu onun otuz oysa pek rağmen sadece sanki sekiz seksen sen senden seni senin siz sizden sizi sizin tarafından trilyon tüm var vardı ve veya ya yani yapacak yapmak yaptı yaptıkları yaptığı yaptığını yapılan yapılması yapıyor yedi yerine yetmiş yine yirmi yoksa yüz zaten çok çünkü öyle üzere üç şey şeyden şeyi şeyler şu şuna şunda şundan şunları şunu şöyle".split(" ")),r.Pipeline.registerFunction(r.tr.stopWordFilter,"stopWordFilter-tr")}}); \ No newline at end of file diff --git a/v1/assets/javascripts/lunr/tinyseg.js b/v1/assets/javascripts/lunr/tinyseg.js new file mode 100644 index 000000000..f7ec60326 --- /dev/null +++ b/v1/assets/javascripts/lunr/tinyseg.js @@ -0,0 +1 @@ +!function(_,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(_.lunr)}(this,function(){return function(_){function t(){var _={"[一二三四五六七八九十百千万億兆]":"M","[一-龠々〆ヵヶ]":"H","[ぁ-ん]":"I","[ァ-ヴーア-ン゙ー]":"K","[a-zA-Za-zA-Z]":"A","[0-90-9]":"N"};for(var t in this.chartype_=[],_){var H=new RegExp;H.compile(t),this.chartype_.push([H,_[t]])}return this.BIAS__=-332,this.BC1__={HH:6,II:2461,KH:406,OH:-1378},this.BC2__={AA:-3267,AI:2744,AN:-878,HH:-4070,HM:-1711,HN:4012,HO:3761,IA:1327,IH:-1184,II:-1332,IK:1721,IO:5492,KI:3831,KK:-8741,MH:-3132,MK:3334,OO:-2920},this.BC3__={HH:996,HI:626,HK:-721,HN:-1307,HO:-836,IH:-301,KK:2762,MK:1079,MM:4034,OA:-1652,OH:266},this.BP1__={BB:295,OB:304,OO:-125,UB:352},this.BP2__={BO:60,OO:-1762},this.BQ1__={BHH:1150,BHM:1521,BII:-1158,BIM:886,BMH:1208,BNH:449,BOH:-91,BOO:-2597,OHI:451,OIH:-296,OKA:1851,OKH:-1020,OKK:904,OOO:2965},this.BQ2__={BHH:118,BHI:-1159,BHM:466,BIH:-919,BKK:-1720,BKO:864,OHH:-1139,OHM:-181,OIH:153,UHI:-1146},this.BQ3__={BHH:-792,BHI:2664,BII:-299,BKI:419,BMH:937,BMM:8335,BNN:998,BOH:775,OHH:2174,OHM:439,OII:280,OKH:1798,OKI:-793,OKO:-2242,OMH:-2402,OOO:11699},this.BQ4__={BHH:-3895,BIH:3761,BII:-4654,BIK:1348,BKK:-1806,BMI:-3385,BOO:-12396,OAH:926,OHH:266,OHK:-2036,ONN:-973},this.BW1__={",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682},this.BW2__={"..":-11822,11:-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669},this.BW3__={"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1e3,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990},this.TC1__={AAA:1093,HHH:1029,HHM:580,HII:998,HOH:-390,HOM:-331,IHI:1169,IOH:-142,IOI:-1015,IOM:467,MMH:187,OOI:-1832},this.TC2__={HHO:2088,HII:-1023,HMM:-1154,IHI:-1965,KKH:703,OII:-2649},this.TC3__={AAA:-294,HHH:346,HHI:-341,HII:-1088,HIK:731,HOH:-1486,IHH:128,IHI:-3041,IHO:-1935,IIH:-825,IIM:-1035,IOI:-542,KHH:-1216,KKA:491,KKH:-1217,KOK:-1009,MHH:-2694,MHM:-457,MHO:123,MMH:-471,NNH:-1689,NNO:662,OHO:-3393},this.TC4__={HHH:-203,HHI:1344,HHK:365,HHM:-122,HHN:182,HHO:669,HIH:804,HII:679,HOH:446,IHH:695,IHO:-2324,IIH:321,III:1497,IIO:656,IOO:54,KAK:4845,KKA:3386,KKK:3065,MHH:-405,MHI:201,MMH:-241,MMM:661,MOM:841},this.TQ1__={BHHH:-227,BHHI:316,BHIH:-132,BIHH:60,BIII:1595,BNHH:-744,BOHH:225,BOOO:-908,OAKK:482,OHHH:281,OHIH:249,OIHI:200,OIIH:-68},this.TQ2__={BIHH:-1401,BIII:-1033,BKAK:-543,BOOO:-5591},this.TQ3__={BHHH:478,BHHM:-1073,BHIH:222,BHII:-504,BIIH:-116,BIII:-105,BMHI:-863,BMHM:-464,BOMH:620,OHHH:346,OHHI:1729,OHII:997,OHMH:481,OIHH:623,OIIH:1344,OKAK:2792,OKHH:587,OKKA:679,OOHH:110,OOII:-685},this.TQ4__={BHHH:-721,BHHM:-3604,BHII:-966,BIIH:-607,BIII:-2181,OAAA:-2763,OAKK:180,OHHH:-294,OHHI:2446,OHHO:480,OHIH:-1573,OIHH:1935,OIHI:-493,OIIH:626,OIII:-4007,OKAK:-8156},this.TW1__={"につい":-4681,"東京都":2026},this.TW2__={"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216},this.TW3__={"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287},this.TW4__={"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865},this.UC1__={A:484,K:93,M:645,O:-505},this.UC2__={A:819,H:1059,I:409,M:3987,N:5775,O:646},this.UC3__={A:-1370,I:2311},this.UC4__={A:-2643,H:1809,I:-1032,K:-3450,M:3565,N:3876,O:6646},this.UC5__={H:313,I:-1238,K:-799,M:539,O:-831},this.UC6__={H:-506,I:-253,K:87,M:247,O:-387},this.UP1__={O:-214},this.UP2__={B:69,O:935},this.UP3__={B:189},this.UQ1__={BH:21,BI:-12,BK:-99,BN:142,BO:-56,OH:-95,OI:477,OK:410,OO:-2422},this.UQ2__={BH:216,BI:113,OK:1759},this.UQ3__={BA:-479,BH:42,BI:1913,BK:-7198,BM:3160,BN:6427,BO:14761,OI:-827,ON:-3212},this.UW1__={",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135},this.UW2__={",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568},this.UW3__={",":4889,1:-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278},this.UW4__={",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1e3,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637},this.UW5__={",":465,".":-299,1:-514,E2:-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343},this.UW6__={",":227,".":808,1:-270,E1:306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496},this}t.prototype.ctype_=function(_){for(var t in this.chartype_)if(_.match(this.chartype_[t][0]))return this.chartype_[t][1];return"O"},t.prototype.ts_=function(_){return _||0},t.prototype.segment=function(_){if(null==_||null==_||""==_)return[];var t=[],H=["B3","B2","B1"],s=["O","O","O"],h=_.split("");for(K=0;K0&&(t.push(i),i="",N="B"),I=O,O=B,B=N,i+=H[K]}return t.push(i),t},_.TinySegmenter=t}}); \ No newline at end of file diff --git a/v1/assets/javascripts/modernizr.1aa3b519.js b/v1/assets/javascripts/modernizr.1aa3b519.js new file mode 100644 index 000000000..14e111fc3 --- /dev/null +++ b/v1/assets/javascripts/modernizr.1aa3b519.js @@ -0,0 +1 @@ +!function(e,t){for(var n in t)e[n]=t[n]}(window,function(e){function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}var n={};return t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{configurable:!1,enumerable:!0,get:r})},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},t.p="",t(t.s=4)}({4:function(e,t,n){"use strict";n(5)},5:function(e,t){!function(t){!function(e,t,n){function r(e,t){return typeof e===t}function o(e){var t=_.className,n=C._config.classPrefix||"";if(T&&(t=t.baseVal),C._config.enableJSClass){var r=new RegExp("(^|\\s)"+n+"no-js(\\s|$)");t=t.replace(r,"$1"+n+"js$2")}C._config.enableClasses&&(t+=" "+n+e.join(" "+n),T?_.className.baseVal=t:_.className=t)}function i(e,t){if("object"==typeof e)for(var n in e)b(e,n)&&i(n,e[n]);else{e=e.toLowerCase();var r=e.split("."),s=C[r[0]];if(2==r.length&&(s=s[r[1]]),void 0!==s)return C;t="function"==typeof t?t():t,1==r.length?C[r[0]]=t:(!C[r[0]]||C[r[0]]instanceof Boolean||(C[r[0]]=new Boolean(C[r[0]])),C[r[0]][r[1]]=t),o([(t&&0!=t?"":"no-")+r.join("-")]),C._trigger(e,t)}return C}function s(){return"function"!=typeof t.createElement?t.createElement(arguments[0]):T?t.createElementNS.call(t,"http://www.w3.org/2000/svg",arguments[0]):t.createElement.apply(t,arguments)}function a(){var e=t.body;return e||(e=s(T?"svg":"body"),e.fake=!0),e}function u(e,n,r,o){var i,u,l,f,c="modernizr",d=s("div"),p=a();if(parseInt(r,10))for(;r--;)l=s("div"),l.id=o?o[r]:c+(r+1),d.appendChild(l);return i=s("style"),i.type="text/css",i.id="s"+c,(p.fake?p:d).appendChild(i),p.appendChild(d),i.styleSheet?i.styleSheet.cssText=e:i.appendChild(t.createTextNode(e)),d.id=c,p.fake&&(p.style.background="",p.style.overflow="hidden",f=_.style.overflow,_.style.overflow="hidden",_.appendChild(p)),u=n(d,e),p.fake?(p.parentNode.removeChild(p),_.style.overflow=f,_.offsetHeight):d.parentNode.removeChild(d),!!u}function l(e,t){return!!~(""+e).indexOf(t)}function f(e){return e.replace(/([A-Z])/g,function(e,t){return"-"+t.toLowerCase()}).replace(/^ms-/,"-ms-")}function c(t,n,r){var o;if("getComputedStyle"in e){o=getComputedStyle.call(e,t,n);var i=e.console;if(null!==o)r&&(o=o.getPropertyValue(r));else if(i){var s=i.error?"error":"log";i[s].call(i,"getComputedStyle returning null, its possible modernizr test results are inaccurate")}}else o=!n&&t.currentStyle&&t.currentStyle[r];return o}function d(t,r){var o=t.length;if("CSS"in e&&"supports"in e.CSS){for(;o--;)if(e.CSS.supports(f(t[o]),r))return!0;return!1}if("CSSSupportsRule"in e){for(var i=[];o--;)i.push("("+f(t[o])+":"+r+")");return i=i.join(" or "),u("@supports ("+i+") { #modernizr { position: absolute; } }",function(e){return"absolute"==c(e,null,"position")})}return n}function p(e){return e.replace(/([a-z])-([a-z])/g,function(e,t,n){return t+n.toUpperCase()}).replace(/^-/,"")}function h(e,t,o,i){function a(){f&&(delete j.style,delete j.modElem)}if(i=!r(i,"undefined")&&i,!r(o,"undefined")){var u=d(e,o);if(!r(u,"undefined"))return u}for(var f,c,h,m,v,g=["modernizr","tspan","samp"];!j.style&&g.length;)f=!0,j.modElem=s(g.shift()),j.style=j.modElem.style;for(h=e.length,c=0;h>c;c++)if(m=e[c],v=j.style[m],l(m,"-")&&(m=p(m)),j.style[m]!==n){if(i||r(o,"undefined"))return a(),"pfx"!=t||m;try{j.style[m]=o}catch(e){}if(j.style[m]!=v)return a(),"pfx"!=t||m}return a(),!1}function m(e,t){return function(){return e.apply(t,arguments)}}function v(e,t,n){var o;for(var i in e)if(e[i]in t)return!1===n?e[i]:(o=t[e[i]],r(o,"function")?m(o,n||t):o);return!1}function g(e,t,n,o,i){var s=e.charAt(0).toUpperCase()+e.slice(1),a=(e+" "+k.join(s+" ")+s).split(" ");return r(t,"string")||r(t,"undefined")?h(a,t,o,i):(a=(e+" "+A.join(s+" ")+s).split(" "),v(a,t,n))}function y(e,t,r){return g(e,n,n,t,r)}var w=[],S={_version:"3.5.0",_config:{classPrefix:"",enableClasses:!0,enableJSClass:!0,usePrefixes:!0},_q:[],on:function(e,t){var n=this;setTimeout(function(){t(n[e])},0)},addTest:function(e,t,n){w.push({name:e,fn:t,options:n})},addAsyncTest:function(e){w.push({name:null,fn:e})}},C=function(){};C.prototype=S,C=new C;var b,x=[],_=t.documentElement,T="svg"===_.nodeName.toLowerCase();!function(){var e={}.hasOwnProperty;b=r(e,"undefined")||r(e.call,"undefined")?function(e,t){return t in e&&r(e.constructor.prototype[t],"undefined")}:function(t,n){return e.call(t,n)}}(),S._l={},S.on=function(e,t){this._l[e]||(this._l[e]=[]),this._l[e].push(t),C.hasOwnProperty(e)&&setTimeout(function(){C._trigger(e,C[e])},0)},S._trigger=function(e,t){if(this._l[e]){var n=this._l[e];setTimeout(function(){var e;for(e=0;e .md-nav__link { + color: inherit; } + +button[data-md-color-primary="pink"] { + background-color: #e91e63; } + +[data-md-color-primary="pink"] .md-typeset a { + color: #e91e63; } + +[data-md-color-primary="pink"] .md-header { + background-color: #e91e63; } + +[data-md-color-primary="pink"] .md-hero { + background-color: #e91e63; } + +[data-md-color-primary="pink"] .md-nav__link:active, +[data-md-color-primary="pink"] .md-nav__link--active { + color: #e91e63; } + +[data-md-color-primary="pink"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="purple"] { + background-color: #ab47bc; } + +[data-md-color-primary="purple"] .md-typeset a { + color: #ab47bc; } + +[data-md-color-primary="purple"] .md-header { + background-color: #ab47bc; } + +[data-md-color-primary="purple"] .md-hero { + background-color: #ab47bc; } + +[data-md-color-primary="purple"] .md-nav__link:active, +[data-md-color-primary="purple"] .md-nav__link--active { + color: #ab47bc; } + +[data-md-color-primary="purple"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="deep-purple"] { + background-color: #7e57c2; } + +[data-md-color-primary="deep-purple"] .md-typeset a { + color: #7e57c2; } + +[data-md-color-primary="deep-purple"] .md-header { + background-color: #7e57c2; } + +[data-md-color-primary="deep-purple"] .md-hero { + background-color: #7e57c2; } + +[data-md-color-primary="deep-purple"] .md-nav__link:active, +[data-md-color-primary="deep-purple"] .md-nav__link--active { + color: #7e57c2; } + +[data-md-color-primary="deep-purple"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="indigo"] { + background-color: #3f51b5; } + +[data-md-color-primary="indigo"] .md-typeset a { + color: #3f51b5; } + +[data-md-color-primary="indigo"] .md-header { + background-color: #3f51b5; } + +[data-md-color-primary="indigo"] .md-hero { + background-color: #3f51b5; } + +[data-md-color-primary="indigo"] .md-nav__link:active, +[data-md-color-primary="indigo"] .md-nav__link--active { + color: #3f51b5; } + +[data-md-color-primary="indigo"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="blue"] { + background-color: #2196f3; } + +[data-md-color-primary="blue"] .md-typeset a { + color: #2196f3; } + +[data-md-color-primary="blue"] .md-header { + background-color: #2196f3; } + +[data-md-color-primary="blue"] .md-hero { + background-color: #2196f3; } + +[data-md-color-primary="blue"] .md-nav__link:active, +[data-md-color-primary="blue"] .md-nav__link--active { + color: #2196f3; } + +[data-md-color-primary="blue"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="light-blue"] { + background-color: #03a9f4; } + +[data-md-color-primary="light-blue"] .md-typeset a { + color: #03a9f4; } + +[data-md-color-primary="light-blue"] .md-header { + background-color: #03a9f4; } + +[data-md-color-primary="light-blue"] .md-hero { + background-color: #03a9f4; } + +[data-md-color-primary="light-blue"] .md-nav__link:active, +[data-md-color-primary="light-blue"] .md-nav__link--active { + color: #03a9f4; } + +[data-md-color-primary="light-blue"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="cyan"] { + background-color: #00bcd4; } + +[data-md-color-primary="cyan"] .md-typeset a { + color: #00bcd4; } + +[data-md-color-primary="cyan"] .md-header { + background-color: #00bcd4; } + +[data-md-color-primary="cyan"] .md-hero { + background-color: #00bcd4; } + +[data-md-color-primary="cyan"] .md-nav__link:active, +[data-md-color-primary="cyan"] .md-nav__link--active { + color: #00bcd4; } + +[data-md-color-primary="cyan"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="teal"] { + background-color: #009688; } + +[data-md-color-primary="teal"] .md-typeset a { + color: #009688; } + +[data-md-color-primary="teal"] .md-header { + background-color: #009688; } + +[data-md-color-primary="teal"] .md-hero { + background-color: #009688; } + +[data-md-color-primary="teal"] .md-nav__link:active, +[data-md-color-primary="teal"] .md-nav__link--active { + color: #009688; } + +[data-md-color-primary="teal"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="green"] { + background-color: #4caf50; } + +[data-md-color-primary="green"] .md-typeset a { + color: #4caf50; } + +[data-md-color-primary="green"] .md-header { + background-color: #4caf50; } + +[data-md-color-primary="green"] .md-hero { + background-color: #4caf50; } + +[data-md-color-primary="green"] .md-nav__link:active, +[data-md-color-primary="green"] .md-nav__link--active { + color: #4caf50; } + +[data-md-color-primary="green"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="light-green"] { + background-color: #7cb342; } + +[data-md-color-primary="light-green"] .md-typeset a { + color: #7cb342; } + +[data-md-color-primary="light-green"] .md-header { + background-color: #7cb342; } + +[data-md-color-primary="light-green"] .md-hero { + background-color: #7cb342; } + +[data-md-color-primary="light-green"] .md-nav__link:active, +[data-md-color-primary="light-green"] .md-nav__link--active { + color: #7cb342; } + +[data-md-color-primary="light-green"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="lime"] { + background-color: #c0ca33; } + +[data-md-color-primary="lime"] .md-typeset a { + color: #c0ca33; } + +[data-md-color-primary="lime"] .md-header { + background-color: #c0ca33; } + +[data-md-color-primary="lime"] .md-hero { + background-color: #c0ca33; } + +[data-md-color-primary="lime"] .md-nav__link:active, +[data-md-color-primary="lime"] .md-nav__link--active { + color: #c0ca33; } + +[data-md-color-primary="lime"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="yellow"] { + background-color: #f9a825; } + +[data-md-color-primary="yellow"] .md-typeset a { + color: #f9a825; } + +[data-md-color-primary="yellow"] .md-header { + background-color: #f9a825; } + +[data-md-color-primary="yellow"] .md-hero { + background-color: #f9a825; } + +[data-md-color-primary="yellow"] .md-nav__link:active, +[data-md-color-primary="yellow"] .md-nav__link--active { + color: #f9a825; } + +[data-md-color-primary="yellow"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="amber"] { + background-color: #ffa000; } + +[data-md-color-primary="amber"] .md-typeset a { + color: #ffa000; } + +[data-md-color-primary="amber"] .md-header { + background-color: #ffa000; } + +[data-md-color-primary="amber"] .md-hero { + background-color: #ffa000; } + +[data-md-color-primary="amber"] .md-nav__link:active, +[data-md-color-primary="amber"] .md-nav__link--active { + color: #ffa000; } + +[data-md-color-primary="amber"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="orange"] { + background-color: #fb8c00; } + +[data-md-color-primary="orange"] .md-typeset a { + color: #fb8c00; } + +[data-md-color-primary="orange"] .md-header { + background-color: #fb8c00; } + +[data-md-color-primary="orange"] .md-hero { + background-color: #fb8c00; } + +[data-md-color-primary="orange"] .md-nav__link:active, +[data-md-color-primary="orange"] .md-nav__link--active { + color: #fb8c00; } + +[data-md-color-primary="orange"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="deep-orange"] { + background-color: #ff7043; } + +[data-md-color-primary="deep-orange"] .md-typeset a { + color: #ff7043; } + +[data-md-color-primary="deep-orange"] .md-header { + background-color: #ff7043; } + +[data-md-color-primary="deep-orange"] .md-hero { + background-color: #ff7043; } + +[data-md-color-primary="deep-orange"] .md-nav__link:active, +[data-md-color-primary="deep-orange"] .md-nav__link--active { + color: #ff7043; } + +[data-md-color-primary="deep-orange"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="brown"] { + background-color: #795548; } + +[data-md-color-primary="brown"] .md-typeset a { + color: #795548; } + +[data-md-color-primary="brown"] .md-header { + background-color: #795548; } + +[data-md-color-primary="brown"] .md-hero { + background-color: #795548; } + +[data-md-color-primary="brown"] .md-nav__link:active, +[data-md-color-primary="brown"] .md-nav__link--active { + color: #795548; } + +[data-md-color-primary="brown"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="grey"] { + background-color: #757575; } + +[data-md-color-primary="grey"] .md-typeset a { + color: #757575; } + +[data-md-color-primary="grey"] .md-header { + background-color: #757575; } + +[data-md-color-primary="grey"] .md-hero { + background-color: #757575; } + +[data-md-color-primary="grey"] .md-nav__link:active, +[data-md-color-primary="grey"] .md-nav__link--active { + color: #757575; } + +[data-md-color-primary="grey"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="blue-grey"] { + background-color: #546e7a; } + +[data-md-color-primary="blue-grey"] .md-typeset a { + color: #546e7a; } + +[data-md-color-primary="blue-grey"] .md-header { + background-color: #546e7a; } + +[data-md-color-primary="blue-grey"] .md-hero { + background-color: #546e7a; } + +[data-md-color-primary="blue-grey"] .md-nav__link:active, +[data-md-color-primary="blue-grey"] .md-nav__link--active { + color: #546e7a; } + +[data-md-color-primary="blue-grey"] .md-nav__item--nested > .md-nav__link { + color: inherit; } + +button[data-md-color-primary="white"] { + background-color: white; + color: rgba(0, 0, 0, 0.87); + box-shadow: 0 0 0.1rem rgba(0, 0, 0, 0.54) inset; } + +[data-md-color-primary="white"] .md-header { + background-color: white; + color: rgba(0, 0, 0, 0.87); } + +[data-md-color-primary="white"] .md-hero { + background-color: white; + color: rgba(0, 0, 0, 0.87); } + [data-md-color-primary="white"] .md-hero--expand { + border-bottom: 0.1rem solid rgba(0, 0, 0, 0.07); } + +button[data-md-color-accent="red"] { + background-color: #ff1744; } + +[data-md-color-accent="red"] .md-typeset a:hover, +[data-md-color-accent="red"] .md-typeset a:active { + color: #ff1744; } + +[data-md-color-accent="red"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="red"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #ff1744; } + +[data-md-color-accent="red"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="red"] .md-typeset .md-clipboard:active::before { + color: #ff1744; } + +[data-md-color-accent="red"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="red"] .md-typeset .footnote li:target .footnote-backref { + color: #ff1744; } + +[data-md-color-accent="red"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="red"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="red"] .md-typeset [id] .headerlink:focus { + color: #ff1744; } + +[data-md-color-accent="red"] .md-nav__link:focus, +[data-md-color-accent="red"] .md-nav__link:hover { + color: #ff1744; } + +[data-md-color-accent="red"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ff1744; } + +[data-md-color-accent="red"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="red"] .md-search-result__link:hover { + background-color: rgba(255, 23, 68, 0.1); } + +[data-md-color-accent="red"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ff1744; } + +[data-md-color-accent="red"] .md-source-file:hover::before { + background-color: #ff1744; } + +button[data-md-color-accent="pink"] { + background-color: #f50057; } + +[data-md-color-accent="pink"] .md-typeset a:hover, +[data-md-color-accent="pink"] .md-typeset a:active { + color: #f50057; } + +[data-md-color-accent="pink"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="pink"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #f50057; } + +[data-md-color-accent="pink"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="pink"] .md-typeset .md-clipboard:active::before { + color: #f50057; } + +[data-md-color-accent="pink"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="pink"] .md-typeset .footnote li:target .footnote-backref { + color: #f50057; } + +[data-md-color-accent="pink"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="pink"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="pink"] .md-typeset [id] .headerlink:focus { + color: #f50057; } + +[data-md-color-accent="pink"] .md-nav__link:focus, +[data-md-color-accent="pink"] .md-nav__link:hover { + color: #f50057; } + +[data-md-color-accent="pink"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #f50057; } + +[data-md-color-accent="pink"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="pink"] .md-search-result__link:hover { + background-color: rgba(245, 0, 87, 0.1); } + +[data-md-color-accent="pink"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #f50057; } + +[data-md-color-accent="pink"] .md-source-file:hover::before { + background-color: #f50057; } + +button[data-md-color-accent="purple"] { + background-color: #e040fb; } + +[data-md-color-accent="purple"] .md-typeset a:hover, +[data-md-color-accent="purple"] .md-typeset a:active { + color: #e040fb; } + +[data-md-color-accent="purple"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="purple"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #e040fb; } + +[data-md-color-accent="purple"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="purple"] .md-typeset .md-clipboard:active::before { + color: #e040fb; } + +[data-md-color-accent="purple"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="purple"] .md-typeset .footnote li:target .footnote-backref { + color: #e040fb; } + +[data-md-color-accent="purple"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="purple"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="purple"] .md-typeset [id] .headerlink:focus { + color: #e040fb; } + +[data-md-color-accent="purple"] .md-nav__link:focus, +[data-md-color-accent="purple"] .md-nav__link:hover { + color: #e040fb; } + +[data-md-color-accent="purple"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #e040fb; } + +[data-md-color-accent="purple"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="purple"] .md-search-result__link:hover { + background-color: rgba(224, 64, 251, 0.1); } + +[data-md-color-accent="purple"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #e040fb; } + +[data-md-color-accent="purple"] .md-source-file:hover::before { + background-color: #e040fb; } + +button[data-md-color-accent="deep-purple"] { + background-color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-typeset a:hover, +[data-md-color-accent="deep-purple"] .md-typeset a:active { + color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="deep-purple"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="deep-purple"] .md-typeset .md-clipboard:active::before { + color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="deep-purple"] .md-typeset .footnote li:target .footnote-backref { + color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="deep-purple"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="deep-purple"] .md-typeset [id] .headerlink:focus { + color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-nav__link:focus, +[data-md-color-accent="deep-purple"] .md-nav__link:hover { + color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="deep-purple"] .md-search-result__link:hover { + background-color: rgba(124, 77, 255, 0.1); } + +[data-md-color-accent="deep-purple"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #7c4dff; } + +[data-md-color-accent="deep-purple"] .md-source-file:hover::before { + background-color: #7c4dff; } + +button[data-md-color-accent="indigo"] { + background-color: #536dfe; } + +[data-md-color-accent="indigo"] .md-typeset a:hover, +[data-md-color-accent="indigo"] .md-typeset a:active { + color: #536dfe; } + +[data-md-color-accent="indigo"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="indigo"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + +[data-md-color-accent="indigo"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="indigo"] .md-typeset .md-clipboard:active::before { + color: #536dfe; } + +[data-md-color-accent="indigo"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="indigo"] .md-typeset .footnote li:target .footnote-backref { + color: #536dfe; } + +[data-md-color-accent="indigo"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="indigo"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="indigo"] .md-typeset [id] .headerlink:focus { + color: #536dfe; } + +[data-md-color-accent="indigo"] .md-nav__link:focus, +[data-md-color-accent="indigo"] .md-nav__link:hover { + color: #536dfe; } + +[data-md-color-accent="indigo"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + +[data-md-color-accent="indigo"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="indigo"] .md-search-result__link:hover { + background-color: rgba(83, 109, 254, 0.1); } + +[data-md-color-accent="indigo"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + +[data-md-color-accent="indigo"] .md-source-file:hover::before { + background-color: #536dfe; } + +button[data-md-color-accent="blue"] { + background-color: #448aff; } + +[data-md-color-accent="blue"] .md-typeset a:hover, +[data-md-color-accent="blue"] .md-typeset a:active { + color: #448aff; } + +[data-md-color-accent="blue"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="blue"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #448aff; } + +[data-md-color-accent="blue"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="blue"] .md-typeset .md-clipboard:active::before { + color: #448aff; } + +[data-md-color-accent="blue"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="blue"] .md-typeset .footnote li:target .footnote-backref { + color: #448aff; } + +[data-md-color-accent="blue"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="blue"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="blue"] .md-typeset [id] .headerlink:focus { + color: #448aff; } + +[data-md-color-accent="blue"] .md-nav__link:focus, +[data-md-color-accent="blue"] .md-nav__link:hover { + color: #448aff; } + +[data-md-color-accent="blue"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #448aff; } + +[data-md-color-accent="blue"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="blue"] .md-search-result__link:hover { + background-color: rgba(68, 138, 255, 0.1); } + +[data-md-color-accent="blue"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #448aff; } + +[data-md-color-accent="blue"] .md-source-file:hover::before { + background-color: #448aff; } + +button[data-md-color-accent="light-blue"] { + background-color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-typeset a:hover, +[data-md-color-accent="light-blue"] .md-typeset a:active { + color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="light-blue"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="light-blue"] .md-typeset .md-clipboard:active::before { + color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="light-blue"] .md-typeset .footnote li:target .footnote-backref { + color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="light-blue"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="light-blue"] .md-typeset [id] .headerlink:focus { + color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-nav__link:focus, +[data-md-color-accent="light-blue"] .md-nav__link:hover { + color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="light-blue"] .md-search-result__link:hover { + background-color: rgba(0, 145, 234, 0.1); } + +[data-md-color-accent="light-blue"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #0091ea; } + +[data-md-color-accent="light-blue"] .md-source-file:hover::before { + background-color: #0091ea; } + +button[data-md-color-accent="cyan"] { + background-color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-typeset a:hover, +[data-md-color-accent="cyan"] .md-typeset a:active { + color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="cyan"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="cyan"] .md-typeset .md-clipboard:active::before { + color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="cyan"] .md-typeset .footnote li:target .footnote-backref { + color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="cyan"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="cyan"] .md-typeset [id] .headerlink:focus { + color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-nav__link:focus, +[data-md-color-accent="cyan"] .md-nav__link:hover { + color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="cyan"] .md-search-result__link:hover { + background-color: rgba(0, 184, 212, 0.1); } + +[data-md-color-accent="cyan"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #00b8d4; } + +[data-md-color-accent="cyan"] .md-source-file:hover::before { + background-color: #00b8d4; } + +button[data-md-color-accent="teal"] { + background-color: #00bfa5; } + +[data-md-color-accent="teal"] .md-typeset a:hover, +[data-md-color-accent="teal"] .md-typeset a:active { + color: #00bfa5; } + +[data-md-color-accent="teal"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="teal"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #00bfa5; } + +[data-md-color-accent="teal"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="teal"] .md-typeset .md-clipboard:active::before { + color: #00bfa5; } + +[data-md-color-accent="teal"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="teal"] .md-typeset .footnote li:target .footnote-backref { + color: #00bfa5; } + +[data-md-color-accent="teal"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="teal"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="teal"] .md-typeset [id] .headerlink:focus { + color: #00bfa5; } + +[data-md-color-accent="teal"] .md-nav__link:focus, +[data-md-color-accent="teal"] .md-nav__link:hover { + color: #00bfa5; } + +[data-md-color-accent="teal"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #00bfa5; } + +[data-md-color-accent="teal"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="teal"] .md-search-result__link:hover { + background-color: rgba(0, 191, 165, 0.1); } + +[data-md-color-accent="teal"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #00bfa5; } + +[data-md-color-accent="teal"] .md-source-file:hover::before { + background-color: #00bfa5; } + +button[data-md-color-accent="green"] { + background-color: #00c853; } + +[data-md-color-accent="green"] .md-typeset a:hover, +[data-md-color-accent="green"] .md-typeset a:active { + color: #00c853; } + +[data-md-color-accent="green"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="green"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #00c853; } + +[data-md-color-accent="green"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="green"] .md-typeset .md-clipboard:active::before { + color: #00c853; } + +[data-md-color-accent="green"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="green"] .md-typeset .footnote li:target .footnote-backref { + color: #00c853; } + +[data-md-color-accent="green"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="green"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="green"] .md-typeset [id] .headerlink:focus { + color: #00c853; } + +[data-md-color-accent="green"] .md-nav__link:focus, +[data-md-color-accent="green"] .md-nav__link:hover { + color: #00c853; } + +[data-md-color-accent="green"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #00c853; } + +[data-md-color-accent="green"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="green"] .md-search-result__link:hover { + background-color: rgba(0, 200, 83, 0.1); } + +[data-md-color-accent="green"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #00c853; } + +[data-md-color-accent="green"] .md-source-file:hover::before { + background-color: #00c853; } + +button[data-md-color-accent="light-green"] { + background-color: #64dd17; } + +[data-md-color-accent="light-green"] .md-typeset a:hover, +[data-md-color-accent="light-green"] .md-typeset a:active { + color: #64dd17; } + +[data-md-color-accent="light-green"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="light-green"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #64dd17; } + +[data-md-color-accent="light-green"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="light-green"] .md-typeset .md-clipboard:active::before { + color: #64dd17; } + +[data-md-color-accent="light-green"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="light-green"] .md-typeset .footnote li:target .footnote-backref { + color: #64dd17; } + +[data-md-color-accent="light-green"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="light-green"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="light-green"] .md-typeset [id] .headerlink:focus { + color: #64dd17; } + +[data-md-color-accent="light-green"] .md-nav__link:focus, +[data-md-color-accent="light-green"] .md-nav__link:hover { + color: #64dd17; } + +[data-md-color-accent="light-green"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #64dd17; } + +[data-md-color-accent="light-green"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="light-green"] .md-search-result__link:hover { + background-color: rgba(100, 221, 23, 0.1); } + +[data-md-color-accent="light-green"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #64dd17; } + +[data-md-color-accent="light-green"] .md-source-file:hover::before { + background-color: #64dd17; } + +button[data-md-color-accent="lime"] { + background-color: #aeea00; } + +[data-md-color-accent="lime"] .md-typeset a:hover, +[data-md-color-accent="lime"] .md-typeset a:active { + color: #aeea00; } + +[data-md-color-accent="lime"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="lime"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #aeea00; } + +[data-md-color-accent="lime"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="lime"] .md-typeset .md-clipboard:active::before { + color: #aeea00; } + +[data-md-color-accent="lime"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="lime"] .md-typeset .footnote li:target .footnote-backref { + color: #aeea00; } + +[data-md-color-accent="lime"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="lime"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="lime"] .md-typeset [id] .headerlink:focus { + color: #aeea00; } + +[data-md-color-accent="lime"] .md-nav__link:focus, +[data-md-color-accent="lime"] .md-nav__link:hover { + color: #aeea00; } + +[data-md-color-accent="lime"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #aeea00; } + +[data-md-color-accent="lime"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="lime"] .md-search-result__link:hover { + background-color: rgba(174, 234, 0, 0.1); } + +[data-md-color-accent="lime"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #aeea00; } + +[data-md-color-accent="lime"] .md-source-file:hover::before { + background-color: #aeea00; } + +button[data-md-color-accent="yellow"] { + background-color: #ffd600; } + +[data-md-color-accent="yellow"] .md-typeset a:hover, +[data-md-color-accent="yellow"] .md-typeset a:active { + color: #ffd600; } + +[data-md-color-accent="yellow"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="yellow"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #ffd600; } + +[data-md-color-accent="yellow"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="yellow"] .md-typeset .md-clipboard:active::before { + color: #ffd600; } + +[data-md-color-accent="yellow"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="yellow"] .md-typeset .footnote li:target .footnote-backref { + color: #ffd600; } + +[data-md-color-accent="yellow"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="yellow"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="yellow"] .md-typeset [id] .headerlink:focus { + color: #ffd600; } + +[data-md-color-accent="yellow"] .md-nav__link:focus, +[data-md-color-accent="yellow"] .md-nav__link:hover { + color: #ffd600; } + +[data-md-color-accent="yellow"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ffd600; } + +[data-md-color-accent="yellow"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="yellow"] .md-search-result__link:hover { + background-color: rgba(255, 214, 0, 0.1); } + +[data-md-color-accent="yellow"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ffd600; } + +[data-md-color-accent="yellow"] .md-source-file:hover::before { + background-color: #ffd600; } + +button[data-md-color-accent="amber"] { + background-color: #ffab00; } + +[data-md-color-accent="amber"] .md-typeset a:hover, +[data-md-color-accent="amber"] .md-typeset a:active { + color: #ffab00; } + +[data-md-color-accent="amber"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="amber"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #ffab00; } + +[data-md-color-accent="amber"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="amber"] .md-typeset .md-clipboard:active::before { + color: #ffab00; } + +[data-md-color-accent="amber"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="amber"] .md-typeset .footnote li:target .footnote-backref { + color: #ffab00; } + +[data-md-color-accent="amber"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="amber"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="amber"] .md-typeset [id] .headerlink:focus { + color: #ffab00; } + +[data-md-color-accent="amber"] .md-nav__link:focus, +[data-md-color-accent="amber"] .md-nav__link:hover { + color: #ffab00; } + +[data-md-color-accent="amber"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ffab00; } + +[data-md-color-accent="amber"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="amber"] .md-search-result__link:hover { + background-color: rgba(255, 171, 0, 0.1); } + +[data-md-color-accent="amber"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ffab00; } + +[data-md-color-accent="amber"] .md-source-file:hover::before { + background-color: #ffab00; } + +button[data-md-color-accent="orange"] { + background-color: #ff9100; } + +[data-md-color-accent="orange"] .md-typeset a:hover, +[data-md-color-accent="orange"] .md-typeset a:active { + color: #ff9100; } + +[data-md-color-accent="orange"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="orange"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #ff9100; } + +[data-md-color-accent="orange"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="orange"] .md-typeset .md-clipboard:active::before { + color: #ff9100; } + +[data-md-color-accent="orange"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="orange"] .md-typeset .footnote li:target .footnote-backref { + color: #ff9100; } + +[data-md-color-accent="orange"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="orange"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="orange"] .md-typeset [id] .headerlink:focus { + color: #ff9100; } + +[data-md-color-accent="orange"] .md-nav__link:focus, +[data-md-color-accent="orange"] .md-nav__link:hover { + color: #ff9100; } + +[data-md-color-accent="orange"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ff9100; } + +[data-md-color-accent="orange"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="orange"] .md-search-result__link:hover { + background-color: rgba(255, 145, 0, 0.1); } + +[data-md-color-accent="orange"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ff9100; } + +[data-md-color-accent="orange"] .md-source-file:hover::before { + background-color: #ff9100; } + +button[data-md-color-accent="deep-orange"] { + background-color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-typeset a:hover, +[data-md-color-accent="deep-orange"] .md-typeset a:active { + color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-typeset pre code::-webkit-scrollbar-thumb:hover, +[data-md-color-accent="deep-orange"] .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover { + background-color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-typeset .md-clipboard:hover::before, +[data-md-color-accent="deep-orange"] .md-typeset .md-clipboard:active::before { + color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-typeset .footnote li:hover .footnote-backref:hover, +[data-md-color-accent="deep-orange"] .md-typeset .footnote li:target .footnote-backref { + color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-typeset [id]:hover .headerlink:hover, +[data-md-color-accent="deep-orange"] .md-typeset [id]:target .headerlink, +[data-md-color-accent="deep-orange"] .md-typeset [id] .headerlink:focus { + color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-nav__link:focus, +[data-md-color-accent="deep-orange"] .md-nav__link:hover { + color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-search-result__link[data-md-state="active"], [data-md-color-accent="deep-orange"] .md-search-result__link:hover { + background-color: rgba(255, 110, 64, 0.1); } + +[data-md-color-accent="deep-orange"] .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #ff6e40; } + +[data-md-color-accent="deep-orange"] .md-source-file:hover::before { + background-color: #ff6e40; } + +@media only screen and (max-width: 59.9375em) { + [data-md-color-primary="red"] .md-nav__source { + background-color: rgba(190, 66, 64, 0.9675); } + [data-md-color-primary="pink"] .md-nav__source { + background-color: rgba(185, 24, 79, 0.9675); } + [data-md-color-primary="purple"] .md-nav__source { + background-color: rgba(136, 57, 150, 0.9675); } + [data-md-color-primary="deep-purple"] .md-nav__source { + background-color: rgba(100, 69, 154, 0.9675); } + [data-md-color-primary="indigo"] .md-nav__source { + background-color: rgba(50, 64, 144, 0.9675); } + [data-md-color-primary="blue"] .md-nav__source { + background-color: rgba(26, 119, 193, 0.9675); } + [data-md-color-primary="light-blue"] .md-nav__source { + background-color: rgba(2, 134, 194, 0.9675); } + [data-md-color-primary="cyan"] .md-nav__source { + background-color: rgba(0, 150, 169, 0.9675); } + [data-md-color-primary="teal"] .md-nav__source { + background-color: rgba(0, 119, 108, 0.9675); } + [data-md-color-primary="green"] .md-nav__source { + background-color: rgba(60, 139, 64, 0.9675); } + [data-md-color-primary="light-green"] .md-nav__source { + background-color: rgba(99, 142, 53, 0.9675); } + [data-md-color-primary="lime"] .md-nav__source { + background-color: rgba(153, 161, 41, 0.9675); } + [data-md-color-primary="yellow"] .md-nav__source { + background-color: rgba(198, 134, 29, 0.9675); } + [data-md-color-primary="amber"] .md-nav__source { + background-color: rgba(203, 127, 0, 0.9675); } + [data-md-color-primary="orange"] .md-nav__source { + background-color: rgba(200, 111, 0, 0.9675); } + [data-md-color-primary="deep-orange"] .md-nav__source { + background-color: rgba(203, 89, 53, 0.9675); } + [data-md-color-primary="brown"] .md-nav__source { + background-color: rgba(96, 68, 57, 0.9675); } + [data-md-color-primary="grey"] .md-nav__source { + background-color: rgba(93, 93, 93, 0.9675); } + [data-md-color-primary="blue-grey"] .md-nav__source { + background-color: rgba(67, 88, 97, 0.9675); } + [data-md-color-primary="white"] .md-nav__source { + background-color: rgba(0, 0, 0, 0.07); + color: rgba(0, 0, 0, 0.87); } } + +@media only screen and (max-width: 76.1875em) { + html [data-md-color-primary="red"] .md-nav--primary .md-nav__title--site { + background-color: #ef5350; } + html [data-md-color-primary="pink"] .md-nav--primary .md-nav__title--site { + background-color: #e91e63; } + html [data-md-color-primary="purple"] .md-nav--primary .md-nav__title--site { + background-color: #ab47bc; } + html [data-md-color-primary="deep-purple"] .md-nav--primary .md-nav__title--site { + background-color: #7e57c2; } + html [data-md-color-primary="indigo"] .md-nav--primary .md-nav__title--site { + background-color: #3f51b5; } + html [data-md-color-primary="blue"] .md-nav--primary .md-nav__title--site { + background-color: #2196f3; } + html [data-md-color-primary="light-blue"] .md-nav--primary .md-nav__title--site { + background-color: #03a9f4; } + html [data-md-color-primary="cyan"] .md-nav--primary .md-nav__title--site { + background-color: #00bcd4; } + html [data-md-color-primary="teal"] .md-nav--primary .md-nav__title--site { + background-color: #009688; } + html [data-md-color-primary="green"] .md-nav--primary .md-nav__title--site { + background-color: #4caf50; } + html [data-md-color-primary="light-green"] .md-nav--primary .md-nav__title--site { + background-color: #7cb342; } + html [data-md-color-primary="lime"] .md-nav--primary .md-nav__title--site { + background-color: #c0ca33; } + html [data-md-color-primary="yellow"] .md-nav--primary .md-nav__title--site { + background-color: #f9a825; } + html [data-md-color-primary="amber"] .md-nav--primary .md-nav__title--site { + background-color: #ffa000; } + html [data-md-color-primary="orange"] .md-nav--primary .md-nav__title--site { + background-color: #fb8c00; } + html [data-md-color-primary="deep-orange"] .md-nav--primary .md-nav__title--site { + background-color: #ff7043; } + html [data-md-color-primary="brown"] .md-nav--primary .md-nav__title--site { + background-color: #795548; } + html [data-md-color-primary="grey"] .md-nav--primary .md-nav__title--site { + background-color: #757575; } + html [data-md-color-primary="blue-grey"] .md-nav--primary .md-nav__title--site { + background-color: #546e7a; } + html [data-md-color-primary="white"] .md-nav--primary .md-nav__title--site { + background-color: white; + color: rgba(0, 0, 0, 0.87); } + [data-md-color-primary="white"] .md-hero { + border-bottom: 0.1rem solid rgba(0, 0, 0, 0.07); } } + +@media only screen and (min-width: 76.25em) { + [data-md-color-primary="red"] .md-tabs { + background-color: #ef5350; } + [data-md-color-primary="pink"] .md-tabs { + background-color: #e91e63; } + [data-md-color-primary="purple"] .md-tabs { + background-color: #ab47bc; } + [data-md-color-primary="deep-purple"] .md-tabs { + background-color: #7e57c2; } + [data-md-color-primary="indigo"] .md-tabs { + background-color: #3f51b5; } + [data-md-color-primary="blue"] .md-tabs { + background-color: #2196f3; } + [data-md-color-primary="light-blue"] .md-tabs { + background-color: #03a9f4; } + [data-md-color-primary="cyan"] .md-tabs { + background-color: #00bcd4; } + [data-md-color-primary="teal"] .md-tabs { + background-color: #009688; } + [data-md-color-primary="green"] .md-tabs { + background-color: #4caf50; } + [data-md-color-primary="light-green"] .md-tabs { + background-color: #7cb342; } + [data-md-color-primary="lime"] .md-tabs { + background-color: #c0ca33; } + [data-md-color-primary="yellow"] .md-tabs { + background-color: #f9a825; } + [data-md-color-primary="amber"] .md-tabs { + background-color: #ffa000; } + [data-md-color-primary="orange"] .md-tabs { + background-color: #fb8c00; } + [data-md-color-primary="deep-orange"] .md-tabs { + background-color: #ff7043; } + [data-md-color-primary="brown"] .md-tabs { + background-color: #795548; } + [data-md-color-primary="grey"] .md-tabs { + background-color: #757575; } + [data-md-color-primary="blue-grey"] .md-tabs { + background-color: #546e7a; } + [data-md-color-primary="white"] .md-tabs { + border-bottom: 0.1rem solid rgba(0, 0, 0, 0.07); + background-color: white; + color: rgba(0, 0, 0, 0.87); } } + +@media only screen and (min-width: 60em) { + [data-md-color-primary="white"] .md-search__input { + background-color: rgba(0, 0, 0, 0.07); } + [data-md-color-primary="white"] .md-search__input::-webkit-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + [data-md-color-primary="white"] .md-search__input:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + [data-md-color-primary="white"] .md-search__input::-ms-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + [data-md-color-primary="white"] .md-search__input::placeholder { + color: rgba(0, 0, 0, 0.54); } } + +/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJhc3NldHMvc3R5bGVzaGVldHMvYXBwbGljYXRpb24tcGFsZXR0ZS4yMjkxNTEyNi5jc3MiLCJzb3VyY2VSb290IjoiIn0=*/ \ No newline at end of file diff --git a/v1/assets/stylesheets/application.451f80e5.css b/v1/assets/stylesheets/application.451f80e5.css new file mode 100644 index 000000000..ec0393870 --- /dev/null +++ b/v1/assets/stylesheets/application.451f80e5.css @@ -0,0 +1,2552 @@ +@charset "UTF-8"; +html { + box-sizing: border-box; } + +*, +*::before, +*::after { + box-sizing: inherit; } + +html { + -webkit-text-size-adjust: none; + -moz-text-size-adjust: none; + -ms-text-size-adjust: none; + text-size-adjust: none; } + +body { + margin: 0; } + +hr { + overflow: visible; + box-sizing: content-box; } + +a { + -webkit-text-decoration-skip: objects; } + +a, +button, +label, +input { + -webkit-tap-highlight-color: transparent; } + +a { + color: inherit; + text-decoration: none; } + +small { + font-size: 80%; } + +sub, +sup { + position: relative; + font-size: 80%; + line-height: 0; + vertical-align: baseline; } + +sub { + bottom: -0.25em; } + +sup { + top: -0.5em; } + +img { + border-style: none; } + +table { + border-collapse: separate; + border-spacing: 0; } + +td, +th { + font-weight: normal; + vertical-align: top; } + +button { + margin: 0; + padding: 0; + border: 0; + outline-style: none; + background: transparent; + font-size: inherit; } + +input { + border: 0; + outline: 0; } + +.md-icon, .md-clipboard::before, .md-nav__title::before, .md-nav__button, .md-nav__link::after, .md-search-result__article--document::before, .md-source-file::before, .md-typeset .admonition > .admonition-title::before, .md-typeset details > .admonition-title::before, .md-typeset .admonition > summary::before, .md-typeset details > summary::before, .md-typeset .footnote-backref, .md-typeset .critic.comment::before, .md-typeset summary::after, .md-typeset .task-list-control .task-list-indicator::before { + font-family: "Material Icons"; + font-style: normal; + font-variant: normal; + font-weight: normal; + line-height: 1; + text-transform: none; + white-space: nowrap; + speak: none; + word-wrap: normal; + direction: ltr; } + .md-content__icon, .md-header-nav__button, .md-footer-nav__button, .md-nav__title::before, .md-nav__button, .md-search-result__article--document::before { + display: inline-block; + margin: 0.4rem; + padding: 0.8rem; + font-size: 2.4rem; + cursor: pointer; } + +.md-icon--arrow-back::before { + content: "\E5C4"; } + +.md-icon--arrow-forward::before { + content: "\E5C8"; } + +.md-icon--menu::before { + content: "\E5D2"; } + +.md-icon--search::before { + content: "\E8B6"; } + +[dir="rtl"] .md-icon--arrow-back::before { + content: "\E5C8"; } + +[dir="rtl"] .md-icon--arrow-forward::before { + content: "\E5C4"; } + +body { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } + +body, +input { + color: rgba(0, 0, 0, 0.87); + -webkit-font-feature-settings: "kern", "liga"; + font-feature-settings: "kern", "liga"; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; } + +pre, +code, +kbd { + color: rgba(0, 0, 0, 0.87); + -webkit-font-feature-settings: "kern"; + font-feature-settings: "kern"; + font-family: "Courier New", Courier, monospace; } + +.md-typeset { + font-size: 1.6rem; + line-height: 1.6; + -webkit-print-color-adjust: exact; } + .md-typeset p, + .md-typeset ul, + .md-typeset ol, + .md-typeset blockquote { + margin: 1em 0; } + .md-typeset h1 { + margin: 0 0 4rem; + color: rgba(0, 0, 0, 0.54); + font-size: 3.125rem; + font-weight: 300; + letter-spacing: -0.01em; + line-height: 1.3; } + .md-typeset h2 { + margin: 4rem 0 1.6rem; + font-size: 2.5rem; + font-weight: 300; + letter-spacing: -0.01em; + line-height: 1.4; } + .md-typeset h3 { + margin: 3.2rem 0 1.6rem; + font-size: 2rem; + font-weight: 400; + letter-spacing: -0.01em; + line-height: 1.5; } + .md-typeset h2 + h3 { + margin-top: 1.6rem; } + .md-typeset h4 { + margin: 1.6rem 0; + font-size: 1.6rem; + font-weight: 700; + letter-spacing: -0.01em; } + .md-typeset h5, + .md-typeset h6 { + margin: 1.6rem 0; + color: rgba(0, 0, 0, 0.54); + font-size: 1.28rem; + font-weight: 700; + letter-spacing: -0.01em; } + .md-typeset h5 { + text-transform: uppercase; } + .md-typeset hr { + margin: 1.5em 0; + border-bottom: 0.1rem dotted rgba(0, 0, 0, 0.26); } + .md-typeset a { + color: #3f51b5; + word-break: break-word; } + .md-typeset a, .md-typeset a::before { + transition: color 0.125s; } + .md-typeset a:hover, .md-typeset a:active { + color: #536dfe; } + .md-typeset code, + .md-typeset pre { + background-color: rgba(236, 236, 236, 0.5); + color: #37474F; + font-size: 85%; + direction: ltr; } + .md-typeset code { + margin: 0 0.29412em; + padding: 0.07353em 0; + border-radius: 0.2rem; + box-shadow: 0.29412em 0 0 rgba(236, 236, 236, 0.5), -0.29412em 0 0 rgba(236, 236, 236, 0.5); + word-break: break-word; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; } + .md-typeset h1 code, + .md-typeset h2 code, + .md-typeset h3 code, + .md-typeset h4 code, + .md-typeset h5 code, + .md-typeset h6 code { + margin: 0; + background-color: transparent; + box-shadow: none; } + .md-typeset a > code { + margin: inherit; + padding: inherit; + border-radius: none; + background-color: inherit; + color: inherit; + box-shadow: none; } + .md-typeset pre { + position: relative; + margin: 1em 0; + border-radius: 0.2rem; + line-height: 1.4; + -webkit-overflow-scrolling: touch; } + .md-typeset pre > code { + display: block; + margin: 0; + padding: 1.05rem 1.2rem; + background-color: transparent; + font-size: inherit; + box-shadow: none; + -webkit-box-decoration-break: none; + box-decoration-break: none; + overflow: auto; } + .md-typeset pre > code::-webkit-scrollbar { + width: 0.4rem; + height: 0.4rem; } + .md-typeset pre > code::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.26); } + .md-typeset pre > code::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + .md-typeset kbd { + padding: 0 0.29412em; + border: 0.1rem solid #c9c9c9; + border-radius: 0.3rem; + border-bottom-color: #bcbcbc; + background-color: #FCFCFC; + color: #555555; + font-size: 85%; + box-shadow: 0 0.1rem 0 #b0b0b0; + word-break: break-word; } + .md-typeset mark { + margin: 0 0.25em; + padding: 0.0625em 0; + border-radius: 0.2rem; + background-color: rgba(255, 235, 59, 0.5); + box-shadow: 0.25em 0 0 rgba(255, 235, 59, 0.5), -0.25em 0 0 rgba(255, 235, 59, 0.5); + word-break: break-word; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; } + .md-typeset abbr { + border-bottom: 0.1rem dotted rgba(0, 0, 0, 0.54); + text-decoration: none; + cursor: help; } + .md-typeset small { + opacity: 0.75; } + .md-typeset sup, + .md-typeset sub { + margin-left: 0.07812em; } + [dir="rtl"] .md-typeset sup, [dir="rtl"] + .md-typeset sub { + margin-right: 0.07812em; + margin-left: initial; } + .md-typeset blockquote { + padding-left: 1.2rem; + border-left: 0.4rem solid rgba(0, 0, 0, 0.26); + color: rgba(0, 0, 0, 0.54); } + [dir="rtl"] .md-typeset blockquote { + padding-right: 1.2rem; + padding-left: initial; + border-right: 0.4rem solid rgba(0, 0, 0, 0.26); + border-left: initial; } + .md-typeset ul { + list-style-type: disc; } + .md-typeset ul, + .md-typeset ol { + margin-left: 0.625em; + padding: 0; } + [dir="rtl"] .md-typeset ul, [dir="rtl"] + .md-typeset ol { + margin-right: 0.625em; + margin-left: initial; } + .md-typeset ul ol, + .md-typeset ol ol { + list-style-type: lower-alpha; } + .md-typeset ul ol ol, + .md-typeset ol ol ol { + list-style-type: lower-roman; } + .md-typeset ul li, + .md-typeset ol li { + margin-bottom: 0.5em; + margin-left: 1.25em; } + [dir="rtl"] .md-typeset ul li, [dir="rtl"] + .md-typeset ol li { + margin-right: 1.25em; + margin-left: initial; } + .md-typeset ul li p, + .md-typeset ul li blockquote, + .md-typeset ol li p, + .md-typeset ol li blockquote { + margin: 0.5em 0; } + .md-typeset ul li:last-child, + .md-typeset ol li:last-child { + margin-bottom: 0; } + .md-typeset ul li ul, + .md-typeset ul li ol, + .md-typeset ol li ul, + .md-typeset ol li ol { + margin: 0.5em 0 0.5em 0.625em; } + [dir="rtl"] .md-typeset ul li ul, [dir="rtl"] + .md-typeset ul li ol, [dir="rtl"] + .md-typeset ol li ul, [dir="rtl"] + .md-typeset ol li ol { + margin-right: 0.625em; + margin-left: initial; } + .md-typeset dd { + margin: 1em 0 1em 1.875em; } + [dir="rtl"] .md-typeset dd { + margin-right: 1.875em; + margin-left: initial; } + .md-typeset iframe, + .md-typeset img, + .md-typeset svg { + max-width: 100%; } + .md-typeset table:not([class]) { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + display: inline-block; + max-width: 100%; + border-radius: 0.2rem; + font-size: 1.28rem; + overflow: auto; + -webkit-overflow-scrolling: touch; } + .md-typeset table:not([class]) + * { + margin-top: 1.5em; } + .md-typeset table:not([class]) th:not([align]), + .md-typeset table:not([class]) td:not([align]) { + text-align: left; } + [dir="rtl"] .md-typeset table:not([class]) th:not([align]), [dir="rtl"] + .md-typeset table:not([class]) td:not([align]) { + text-align: right; } + .md-typeset table:not([class]) th { + min-width: 10rem; + padding: 1.2rem 1.6rem; + background-color: rgba(0, 0, 0, 0.54); + color: white; + vertical-align: top; } + .md-typeset table:not([class]) td { + padding: 1.2rem 1.6rem; + border-top: 0.1rem solid rgba(0, 0, 0, 0.07); + vertical-align: top; } + .md-typeset table:not([class]) tr:first-child td { + border-top: 0; } + .md-typeset table:not([class]) a { + word-break: normal; } + .md-typeset__scrollwrap { + margin: 1em -1.6rem; + overflow-x: auto; + -webkit-overflow-scrolling: touch; } + .md-typeset .md-typeset__table { + display: inline-block; + margin-bottom: 0.5em; + padding: 0 1.6rem; } + .md-typeset .md-typeset__table table { + display: table; + width: 100%; + margin: 0; + overflow: hidden; } + +html { + height: 100%; + font-size: 62.5%; + overflow-x: hidden; } + +body { + position: relative; + height: 100%; } + +hr { + display: block; + height: 0.1rem; + padding: 0; + border: 0; } + +.md-svg { + display: none; } + +.md-grid { + max-width: 122rem; + margin-right: auto; + margin-left: auto; } + +.md-container, +.md-main { + overflow: auto; } + +.md-container { + display: table; + width: 100%; + height: 100%; + padding-top: 4.8rem; + table-layout: fixed; } + +.md-main { + display: table-row; + height: 100%; } + .md-main__inner { + height: 100%; + padding-top: 3rem; + padding-bottom: 0.1rem; } + +.md-toggle { + display: none; } + +.md-overlay { + position: fixed; + top: 0; + width: 0; + height: 0; + transition: width 0s 0.25s, height 0s 0.25s, opacity 0.25s; + background-color: rgba(0, 0, 0, 0.54); + opacity: 0; + z-index: 3; } + +.md-flex { + display: table; } + .md-flex__cell { + display: table-cell; + position: relative; + vertical-align: top; } + .md-flex__cell--shrink { + width: 0%; } + .md-flex__cell--stretch { + display: table; + width: 100%; + table-layout: fixed; } + .md-flex__ellipsis { + display: table-cell; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } + +.md-skip { + position: fixed; + width: 0.1rem; + height: 0.1rem; + margin: 1rem; + padding: 0.6rem 1rem; + clip: rect(0.1rem); + -webkit-transform: translateY(0.8rem); + transform: translateY(0.8rem); + border-radius: 0.2rem; + background-color: rgba(0, 0, 0, 0.87); + color: white; + font-size: 1.28rem; + opacity: 0; + overflow: hidden; } + .md-skip:focus { + width: auto; + height: auto; + clip: auto; + -webkit-transform: translateX(0); + transform: translateX(0); + transition: opacity 0.175s 0.075s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.175s 0.075s; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.175s 0.075s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 1; + z-index: 10; } + +@page { + margin: 25mm; } + +.md-clipboard { + position: absolute; + top: 0.6rem; + right: 0.6rem; + width: 2.8rem; + height: 2.8rem; + border-radius: 0.2rem; + font-size: 1.6rem; + cursor: pointer; + z-index: 1; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; } + .md-clipboard::before { + transition: color 0.25s, opacity 0.25s; + color: rgba(0, 0, 0, 0.07); + content: "\E14D"; } + pre:hover .md-clipboard::before, + .codehilite:hover .md-clipboard::before, .md-typeset .highlight:hover .md-clipboard::before { + color: rgba(0, 0, 0, 0.54); } + .md-clipboard:focus::before, .md-clipboard:hover::before { + color: #536dfe; } + .md-clipboard__message { + display: block; + position: absolute; + top: 0; + right: 3.4rem; + padding: 0.6rem 1rem; + -webkit-transform: translateX(0.8rem); + transform: translateX(0.8rem); + transition: opacity 0.175s, -webkit-transform 0.25s cubic-bezier(0.9, 0.1, 0.9, 0); + transition: transform 0.25s cubic-bezier(0.9, 0.1, 0.9, 0), opacity 0.175s; + transition: transform 0.25s cubic-bezier(0.9, 0.1, 0.9, 0), opacity 0.175s, -webkit-transform 0.25s cubic-bezier(0.9, 0.1, 0.9, 0); + border-radius: 0.2rem; + background-color: rgba(0, 0, 0, 0.54); + color: white; + font-size: 1.28rem; + white-space: nowrap; + opacity: 0; + pointer-events: none; } + .md-clipboard__message--active { + -webkit-transform: translateX(0); + transform: translateX(0); + transition: opacity 0.175s 0.075s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.175s 0.075s; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.175s 0.075s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 1; + pointer-events: initial; } + .md-clipboard__message::before { + content: attr(aria-label); } + .md-clipboard__message::after { + display: block; + position: absolute; + top: 50%; + right: -0.4rem; + width: 0; + margin-top: -0.4rem; + border-width: 0.4rem 0 0.4rem 0.4rem; + border-style: solid; + border-color: transparent rgba(0, 0, 0, 0.54); + content: ""; } + +.md-content__inner { + margin: 0 1.6rem 2.4rem; + padding-top: 1.2rem; } + .md-content__inner::before { + display: block; + height: 0.8rem; + content: ""; } + .md-content__inner > :last-child { + margin-bottom: 0; } + +.md-content__icon { + position: relative; + margin: 0.8rem 0; + padding: 0; + float: right; } + .md-typeset .md-content__icon { + color: rgba(0, 0, 0, 0.26); } + +.md-header { + position: fixed; + top: 0; + right: 0; + left: 0; + height: 4.8rem; + transition: background-color 0.25s, color 0.25s; + background-color: #3f51b5; + color: white; + box-shadow: none; + z-index: 2; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; } + .no-js .md-header { + transition: none; + box-shadow: none; } + .md-header[data-md-state="shadow"] { + transition: background-color 0.25s, color 0.25s, box-shadow 0.25s; + box-shadow: 0 0 0.4rem rgba(0, 0, 0, 0.1), 0 0.4rem 0.8rem rgba(0, 0, 0, 0.2); } + +.md-header-nav { + padding: 0 0.4rem; } + .md-header-nav__button { + position: relative; + transition: opacity 0.25s; + z-index: 1; } + .md-header-nav__button:hover { + opacity: 0.7; } + .md-header-nav__button.md-logo * { + display: block; } + .no-js .md-header-nav__button.md-icon--search { + display: none; } + .md-header-nav__topic { + display: block; + position: absolute; + transition: opacity 0.15s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s; + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; } + .md-header-nav__topic + .md-header-nav__topic { + -webkit-transform: translateX(2.5rem); + transform: translateX(2.5rem); + transition: opacity 0.15s, -webkit-transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1); + transition: transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1), opacity 0.15s; + transition: transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1), opacity 0.15s, -webkit-transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1); + opacity: 0; + z-index: -1; + pointer-events: none; } + [dir="rtl"] .md-header-nav__topic + .md-header-nav__topic { + -webkit-transform: translateX(-2.5rem); + transform: translateX(-2.5rem); } + .no-js .md-header-nav__topic { + position: initial; } + .no-js .md-header-nav__topic + .md-header-nav__topic { + display: none; } + .md-header-nav__title { + padding: 0 2rem; + font-size: 1.8rem; + line-height: 4.8rem; } + .md-header-nav__title[data-md-state="active"] .md-header-nav__topic { + -webkit-transform: translateX(-2.5rem); + transform: translateX(-2.5rem); + transition: opacity 0.15s, -webkit-transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1); + transition: transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1), opacity 0.15s; + transition: transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1), opacity 0.15s, -webkit-transform 0.4s cubic-bezier(1, 0.7, 0.1, 0.1); + opacity: 0; + z-index: -1; + pointer-events: none; } + [dir="rtl"] .md-header-nav__title[data-md-state="active"] .md-header-nav__topic { + -webkit-transform: translateX(2.5rem); + transform: translateX(2.5rem); } + .md-header-nav__title[data-md-state="active"] .md-header-nav__topic + .md-header-nav__topic { + -webkit-transform: translateX(0); + transform: translateX(0); + transition: opacity 0.15s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s; + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + opacity: 1; + z-index: 0; + pointer-events: initial; } + .md-header-nav__source { + display: none; } + +.md-hero { + transition: background 0.25s; + background-color: #3f51b5; + color: white; + font-size: 2rem; + overflow: hidden; } + .md-hero__inner { + margin-top: 2rem; + padding: 1.6rem 1.6rem 0.8rem; + transition: opacity 0.25s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.25s; + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.25s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + transition-delay: 0.1s; } + [data-md-state="hidden"] .md-hero__inner { + pointer-events: none; + -webkit-transform: translateY(1.25rem); + transform: translateY(1.25rem); + transition: opacity 0.1s 0s, -webkit-transform 0s 0.4s; + transition: transform 0s 0.4s, opacity 0.1s 0s; + transition: transform 0s 0.4s, opacity 0.1s 0s, -webkit-transform 0s 0.4s; + opacity: 0; } + .md-hero--expand .md-hero__inner { + margin-bottom: 2.4rem; } + +.md-footer-nav { + background-color: rgba(0, 0, 0, 0.87); + color: white; } + .md-footer-nav__inner { + padding: 0.4rem; + overflow: auto; } + .md-footer-nav__link { + padding-top: 2.8rem; + padding-bottom: 0.8rem; + transition: opacity 0.25s; } + .md-footer-nav__link:hover { + opacity: 0.7; } + .md-footer-nav__link--prev { + width: 25%; + float: left; } + [dir="rtl"] .md-footer-nav__link--prev { + float: right; } + .md-footer-nav__link--next { + width: 75%; + float: right; + text-align: right; } + [dir="rtl"] .md-footer-nav__link--next { + float: left; + text-align: left; } + .md-footer-nav__button { + transition: background 0.25s; } + .md-footer-nav__title { + position: relative; + padding: 0 2rem; + font-size: 1.8rem; + line-height: 4.8rem; } + .md-footer-nav__direction { + position: absolute; + right: 0; + left: 0; + margin-top: -2rem; + padding: 0 2rem; + color: rgba(255, 255, 255, 0.7); + font-size: 1.5rem; } + +.md-footer-meta { + background-color: rgba(0, 0, 0, 0.895); } + .md-footer-meta__inner { + padding: 0.4rem; + overflow: auto; } + html .md-footer-meta.md-typeset a { + color: rgba(255, 255, 255, 0.7); } + html .md-footer-meta.md-typeset a:focus, html .md-footer-meta.md-typeset a:hover { + color: white; } + +.md-footer-copyright { + margin: 0 1.2rem; + padding: 0.8rem 0; + color: rgba(255, 255, 255, 0.3); + font-size: 1.28rem; } + .md-footer-copyright__highlight { + color: rgba(255, 255, 255, 0.7); } + +.md-footer-social { + margin: 0 0.8rem; + padding: 0.4rem 0 1.2rem; } + .md-footer-social__link { + display: inline-block; + width: 3.2rem; + height: 3.2rem; + font-size: 1.6rem; + text-align: center; } + .md-footer-social__link::before { + line-height: 1.9; } + +.md-nav { + font-size: 1.4rem; + line-height: 1.3; } + .md-nav__title { + display: block; + padding: 0 1.2rem; + font-weight: 700; + text-overflow: ellipsis; + overflow: hidden; } + .md-nav__title::before { + display: none; + content: "\E5C4"; } + [dir="rtl"] .md-nav__title::before { + content: "\E5C8"; } + .md-nav__title .md-nav__button { + display: none; } + .md-nav__list { + margin: 0; + padding: 0; + list-style: none; } + .md-nav__item { + padding: 0 1.2rem; } + .md-nav__item:last-child { + padding-bottom: 1.2rem; } + .md-nav__item .md-nav__item { + padding-right: 0; } + [dir="rtl"] .md-nav__item .md-nav__item { + padding-right: 1.2rem; + padding-left: 0; } + .md-nav__item .md-nav__item:last-child { + padding-bottom: 0; } + .md-nav__button img { + width: 100%; + height: auto; } + .md-nav__link { + display: block; + margin-top: 0.625em; + transition: color 0.125s; + text-overflow: ellipsis; + cursor: pointer; + overflow: hidden; } + .md-nav__item--nested > .md-nav__link::after { + content: "\E313"; } + html .md-nav__link[for="__toc"] { + display: none; } + html .md-nav__link[for="__toc"] ~ .md-nav { + display: none; } + html .md-nav__link[for="__toc"] + .md-nav__link::after { + display: none; } + .md-nav__link[data-md-state="blur"] { + color: rgba(0, 0, 0, 0.54); } + .md-nav__link:active, .md-nav__link--active { + color: #3f51b5; } + .md-nav__item--nested > .md-nav__link { + color: inherit; } + .md-nav__link:focus, .md-nav__link:hover { + color: #536dfe; } + .md-nav__source { + display: none; } + +.no-js .md-search { + display: none; } + +.md-search__overlay { + opacity: 0; + z-index: 1; } + +.md-search__form { + position: relative; } + +.md-search__input { + position: relative; + padding: 0 4.4rem 0 7.2rem; + text-overflow: ellipsis; + z-index: 2; } + [dir="rtl"] .md-search__input { + padding: 0 7.2rem 0 4.4rem; } + .md-search__input::-webkit-input-placeholder { + transition: color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1); } + .md-search__input:-ms-input-placeholder { + transition: color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1); } + .md-search__input::-ms-input-placeholder { + transition: color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1); } + .md-search__input::placeholder { + transition: color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1); } + .md-search__input ~ .md-search__icon, .md-search__input::-webkit-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + .md-search__input ~ .md-search__icon, .md-search__input:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + .md-search__input ~ .md-search__icon, .md-search__input::-ms-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + .md-search__input ~ .md-search__icon, .md-search__input::placeholder { + color: rgba(0, 0, 0, 0.54); } + .md-search__input::-ms-clear { + display: none; } + +.md-search__icon { + position: absolute; + transition: color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.25s; + font-size: 2.4rem; + cursor: pointer; + z-index: 2; } + .md-search__icon:hover { + opacity: 0.7; } + .md-search__icon[for="__search"] { + top: 0.6rem; + left: 1rem; } + [dir="rtl"] .md-search__icon[for="__search"] { + right: 1rem; + left: initial; } + .md-search__icon[for="__search"]::before { + content: "\E8B6"; } + .md-search__icon[type="reset"] { + top: 0.6rem; + right: 1rem; + -webkit-transform: scale(0.125); + transform: scale(0.125); + transition: opacity 0.15s, -webkit-transform 0.15s cubic-bezier(0.1, 0.7, 0.1, 1); + transition: transform 0.15s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s; + transition: transform 0.15s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s, -webkit-transform 0.15s cubic-bezier(0.1, 0.7, 0.1, 1); + opacity: 0; } + [dir="rtl"] .md-search__icon[type="reset"] { + right: initial; + left: 1rem; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input:valid ~ .md-search__icon[type="reset"] { + -webkit-transform: scale(1); + transform: scale(1); + opacity: 1; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input:valid ~ .md-search__icon[type="reset"]:hover { + opacity: 0.7; } + +.md-search__output { + position: absolute; + width: 100%; + border-radius: 0 0 0.2rem 0.2rem; + overflow: hidden; + z-index: 1; } + +.md-search__scrollwrap { + height: 100%; + background-color: white; + box-shadow: 0 0.1rem 0 rgba(0, 0, 0, 0.07) inset; + overflow-y: auto; + -webkit-overflow-scrolling: touch; } + +.md-search-result { + color: rgba(0, 0, 0, 0.87); + word-break: break-word; } + .md-search-result__meta { + padding: 0 1.6rem; + background-color: rgba(0, 0, 0, 0.07); + color: rgba(0, 0, 0, 0.54); + font-size: 1.28rem; + line-height: 3.6rem; } + .md-search-result__list { + margin: 0; + padding: 0; + border-top: 0.1rem solid rgba(0, 0, 0, 0.07); + list-style: none; } + .md-search-result__item { + box-shadow: 0 -0.1rem 0 rgba(0, 0, 0, 0.07); } + .md-search-result__link { + display: block; + transition: background 0.25s; + outline: 0; + overflow: hidden; } + .md-search-result__link[data-md-state="active"], .md-search-result__link:hover { + background-color: rgba(83, 109, 254, 0.1); } + .md-search-result__link[data-md-state="active"] .md-search-result__article::before, .md-search-result__link:hover .md-search-result__article::before { + opacity: 0.7; } + .md-search-result__link:last-child .md-search-result__teaser { + margin-bottom: 1.2rem; } + .md-search-result__article { + position: relative; + padding: 0 1.6rem; + overflow: auto; } + .md-search-result__article--document::before { + position: absolute; + left: 0; + margin: 0.2rem; + transition: opacity 0.25s; + color: rgba(0, 0, 0, 0.54); + content: "\E880"; } + [dir="rtl"] .md-search-result__article--document::before { + right: 0; + left: initial; } + .md-search-result__article--document .md-search-result__title { + margin: 1.1rem 0; + font-size: 1.6rem; + font-weight: 400; + line-height: 1.4; } + .md-search-result__title { + margin: 0.5em 0; + font-size: 1.28rem; + font-weight: 700; + line-height: 1.4; } + .md-search-result__teaser { + display: -webkit-box; + max-height: 3.3rem; + margin: 0.5em 0; + color: rgba(0, 0, 0, 0.54); + font-size: 1.28rem; + line-height: 1.4; + text-overflow: ellipsis; + overflow: hidden; + -webkit-line-clamp: 2; } + .md-search-result em { + font-style: normal; + font-weight: 700; + text-decoration: underline; } + +.md-sidebar { + position: absolute; + width: 24.2rem; + padding: 2.4rem 0; + overflow: hidden; } + .md-sidebar[data-md-state="lock"] { + position: fixed; + top: 4.8rem; } + .md-sidebar--secondary { + display: none; } + .md-sidebar__scrollwrap { + max-height: 100%; + margin: 0 0.4rem; + overflow-y: auto; + -webkit-backface-visibility: hidden; + backface-visibility: hidden; } + .md-sidebar__scrollwrap::-webkit-scrollbar { + width: 0.4rem; + height: 0.4rem; } + .md-sidebar__scrollwrap::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.26); } + .md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + +@-webkit-keyframes md-source__facts--done { + 0% { + height: 0; } + 100% { + height: 1.3rem; } } + +@keyframes md-source__facts--done { + 0% { + height: 0; } + 100% { + height: 1.3rem; } } + +@-webkit-keyframes md-source__fact--done { + 0% { + -webkit-transform: translateY(100%); + transform: translateY(100%); + opacity: 0; } + 50% { + opacity: 0; } + 100% { + -webkit-transform: translateY(0%); + transform: translateY(0%); + opacity: 1; } } + +@keyframes md-source__fact--done { + 0% { + -webkit-transform: translateY(100%); + transform: translateY(100%); + opacity: 0; } + 50% { + opacity: 0; } + 100% { + -webkit-transform: translateY(0%); + transform: translateY(0%); + opacity: 1; } } + +.md-source { + display: block; + padding-right: 1.2rem; + transition: opacity 0.25s; + font-size: 1.3rem; + line-height: 1.2; + white-space: nowrap; } + [dir="rtl"] .md-source { + padding-right: initial; + padding-left: 1.2rem; } + .md-source:hover { + opacity: 0.7; } + .md-source::after { + display: inline-block; + height: 4.8rem; + content: ""; + vertical-align: middle; } + .md-source__icon { + display: inline-block; + width: 4.8rem; + height: 4.8rem; + content: ""; + vertical-align: middle; } + .md-source__icon svg { + width: 2.4rem; + height: 2.4rem; + margin-top: 1.2rem; + margin-left: 1.2rem; } + [dir="rtl"] .md-source__icon svg { + margin-right: 1.2rem; + margin-left: initial; } + .md-source__icon + .md-source__repository { + margin-left: -4.4rem; + padding-left: 4rem; } + [dir="rtl"] .md-source__icon + .md-source__repository { + margin-right: -4.4rem; + margin-left: initial; + padding-right: 4rem; + padding-left: initial; } + .md-source__repository { + display: inline-block; + max-width: 100%; + margin-left: 1.2rem; + font-weight: 700; + text-overflow: ellipsis; + overflow: hidden; + vertical-align: middle; } + .md-source__facts { + margin: 0; + padding: 0; + font-size: 1.1rem; + font-weight: 700; + list-style-type: none; + opacity: 0.75; + overflow: hidden; } + [data-md-state="done"] .md-source__facts { + -webkit-animation: md-source__facts--done 0.25s ease-in; + animation: md-source__facts--done 0.25s ease-in; } + .md-source__fact { + float: left; } + [dir="rtl"] .md-source__fact { + float: right; } + [data-md-state="done"] .md-source__fact { + -webkit-animation: md-source__fact--done 0.4s ease-out; + animation: md-source__fact--done 0.4s ease-out; } + .md-source__fact::before { + margin: 0 0.2rem; + content: "\B7"; } + .md-source__fact:first-child::before { + display: none; } + +.md-source-file { + display: inline-block; + margin: 1em 0.5em 1em 0; + padding-right: 0.5rem; + border-radius: 0.2rem; + background-color: rgba(0, 0, 0, 0.07); + font-size: 1.28rem; + list-style-type: none; + cursor: pointer; + overflow: hidden; } + .md-source-file::before { + display: inline-block; + margin-right: 0.5rem; + padding: 0.5rem; + background-color: rgba(0, 0, 0, 0.26); + color: white; + font-size: 1.6rem; + content: "\E86F"; + vertical-align: middle; } + html .md-source-file { + transition: background 0.4s, color 0.4s, box-shadow 0.4s cubic-bezier(0.4, 0, 0.2, 1); } + html .md-source-file::before { + transition: inherit; } + html body .md-typeset .md-source-file { + color: rgba(0, 0, 0, 0.54); } + .md-source-file:hover { + box-shadow: 0 0 8px rgba(0, 0, 0, 0.18), 0 8px 16px rgba(0, 0, 0, 0.36); } + .md-source-file:hover::before { + background-color: #536dfe; } + +.md-tabs { + width: 100%; + transition: background 0.25s; + background-color: #3f51b5; + color: white; + overflow: auto; } + .md-tabs__list { + margin: 0; + margin-left: 0.4rem; + padding: 0; + list-style: none; + white-space: nowrap; } + .md-tabs__item { + display: inline-block; + height: 4.8rem; + padding-right: 1.2rem; + padding-left: 1.2rem; } + .md-tabs__link { + display: block; + margin-top: 1.6rem; + transition: opacity 0.25s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.25s; + transition: transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.25s, -webkit-transform 0.4s cubic-bezier(0.1, 0.7, 0.1, 1); + font-size: 1.4rem; + opacity: 0.7; } + .md-tabs__link--active, .md-tabs__link:hover { + color: inherit; + opacity: 1; } + .md-tabs__item:nth-child(2) .md-tabs__link { + transition-delay: 0.02s; } + .md-tabs__item:nth-child(3) .md-tabs__link { + transition-delay: 0.04s; } + .md-tabs__item:nth-child(4) .md-tabs__link { + transition-delay: 0.06s; } + .md-tabs__item:nth-child(5) .md-tabs__link { + transition-delay: 0.08s; } + .md-tabs__item:nth-child(6) .md-tabs__link { + transition-delay: 0.1s; } + .md-tabs__item:nth-child(7) .md-tabs__link { + transition-delay: 0.12s; } + .md-tabs__item:nth-child(8) .md-tabs__link { + transition-delay: 0.14s; } + .md-tabs__item:nth-child(9) .md-tabs__link { + transition-delay: 0.16s; } + .md-tabs__item:nth-child(10) .md-tabs__link { + transition-delay: 0.18s; } + .md-tabs__item:nth-child(11) .md-tabs__link { + transition-delay: 0.2s; } + .md-tabs__item:nth-child(12) .md-tabs__link { + transition-delay: 0.22s; } + .md-tabs__item:nth-child(13) .md-tabs__link { + transition-delay: 0.24s; } + .md-tabs__item:nth-child(14) .md-tabs__link { + transition-delay: 0.26s; } + .md-tabs__item:nth-child(15) .md-tabs__link { + transition-delay: 0.28s; } + .md-tabs__item:nth-child(16) .md-tabs__link { + transition-delay: 0.3s; } + .md-tabs[data-md-state="hidden"] { + pointer-events: none; } + .md-tabs[data-md-state="hidden"] .md-tabs__link { + -webkit-transform: translateY(50%); + transform: translateY(50%); + transition: color 0.25s, opacity 0.1s, -webkit-transform 0s 0.4s; + transition: color 0.25s, transform 0s 0.4s, opacity 0.1s; + transition: color 0.25s, transform 0s 0.4s, opacity 0.1s, -webkit-transform 0s 0.4s; + opacity: 0; } + +.md-typeset .admonition, .md-typeset details { + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 1px 5px 0 rgba(0, 0, 0, 0.12), 0 3px 1px -2px rgba(0, 0, 0, 0.2); + position: relative; + margin: 1.5625em 0; + padding: 0 1.2rem; + border-left: 0.4rem solid #448aff; + border-radius: 0.2rem; + font-size: 1.28rem; + overflow: auto; } + [dir="rtl"] .md-typeset .admonition, [dir="rtl"] .md-typeset details { + border-right: 0.4rem solid #448aff; + border-left: none; } + html .md-typeset .admonition > :last-child, html .md-typeset details > :last-child { + margin-bottom: 1.2rem; } + .md-typeset .admonition .admonition, .md-typeset details .admonition, .md-typeset .admonition details, .md-typeset details details { + margin: 1em 0; } + .md-typeset .admonition > .admonition-title, .md-typeset details > .admonition-title, .md-typeset .admonition > summary, .md-typeset details > summary { + margin: 0 -1.2rem; + padding: 0.8rem 1.2rem 0.8rem 4rem; + border-bottom: 0.1rem solid rgba(68, 138, 255, 0.1); + background-color: rgba(68, 138, 255, 0.1); + font-weight: 700; } + [dir="rtl"] .md-typeset .admonition > .admonition-title, [dir="rtl"] .md-typeset details > .admonition-title, [dir="rtl"] .md-typeset .admonition > summary, [dir="rtl"] .md-typeset details > summary { + padding: 0.8rem 4rem 0.8rem 1.2rem; } + .md-typeset .admonition > .admonition-title:last-child, .md-typeset details > .admonition-title:last-child, .md-typeset .admonition > summary:last-child, .md-typeset details > summary:last-child { + margin-bottom: 0; } + .md-typeset .admonition > .admonition-title::before, .md-typeset details > .admonition-title::before, .md-typeset .admonition > summary::before, .md-typeset details > summary::before { + position: absolute; + left: 1.2rem; + color: #448aff; + font-size: 2rem; + content: "\E3C9"; } + [dir="rtl"] .md-typeset .admonition > .admonition-title::before, [dir="rtl"] .md-typeset details > .admonition-title::before, [dir="rtl"] .md-typeset .admonition > summary::before, [dir="rtl"] .md-typeset details > summary::before { + right: 1.2rem; + left: initial; } + .md-typeset .admonition.summary, .md-typeset details.summary, .md-typeset .admonition.tldr, .md-typeset details.tldr, .md-typeset .admonition.abstract, .md-typeset details.abstract { + border-left-color: #00b0ff; } + [dir="rtl"] .md-typeset .admonition.summary, [dir="rtl"] .md-typeset details.summary, [dir="rtl"] .md-typeset .admonition.tldr, [dir="rtl"] .md-typeset details.tldr, [dir="rtl"] .md-typeset .admonition.abstract, [dir="rtl"] .md-typeset details.abstract { + border-right-color: #00b0ff; } + .md-typeset .admonition.summary > .admonition-title, .md-typeset details.summary > .admonition-title, .md-typeset .admonition.tldr > .admonition-title, .md-typeset details.tldr > .admonition-title, .md-typeset .admonition.summary > summary, .md-typeset details.summary > summary, .md-typeset .admonition.tldr > summary, .md-typeset details.tldr > summary, .md-typeset .admonition.abstract > .admonition-title, .md-typeset details.abstract > .admonition-title, .md-typeset .admonition.abstract > summary, .md-typeset details.abstract > summary { + border-bottom-color: 0.1rem solid rgba(0, 176, 255, 0.1); + background-color: rgba(0, 176, 255, 0.1); } + .md-typeset .admonition.summary > .admonition-title::before, .md-typeset details.summary > .admonition-title::before, .md-typeset .admonition.tldr > .admonition-title::before, .md-typeset details.tldr > .admonition-title::before, .md-typeset .admonition.summary > summary::before, .md-typeset details.summary > summary::before, .md-typeset .admonition.tldr > summary::before, .md-typeset details.tldr > summary::before, .md-typeset .admonition.abstract > .admonition-title::before, .md-typeset details.abstract > .admonition-title::before, .md-typeset .admonition.abstract > summary::before, .md-typeset details.abstract > summary::before { + color: #00b0ff; + content: "\E8D2"; } + .md-typeset .admonition.todo, .md-typeset details.todo, .md-typeset .admonition.info, .md-typeset details.info { + border-left-color: #00b8d4; } + [dir="rtl"] .md-typeset .admonition.todo, [dir="rtl"] .md-typeset details.todo, [dir="rtl"] .md-typeset .admonition.info, [dir="rtl"] .md-typeset details.info { + border-right-color: #00b8d4; } + .md-typeset .admonition.todo > .admonition-title, .md-typeset details.todo > .admonition-title, .md-typeset .admonition.todo > summary, .md-typeset details.todo > summary, .md-typeset .admonition.info > .admonition-title, .md-typeset details.info > .admonition-title, .md-typeset .admonition.info > summary, .md-typeset details.info > summary { + border-bottom-color: 0.1rem solid rgba(0, 184, 212, 0.1); + background-color: rgba(0, 184, 212, 0.1); } + .md-typeset .admonition.todo > .admonition-title::before, .md-typeset details.todo > .admonition-title::before, .md-typeset .admonition.todo > summary::before, .md-typeset details.todo > summary::before, .md-typeset .admonition.info > .admonition-title::before, .md-typeset details.info > .admonition-title::before, .md-typeset .admonition.info > summary::before, .md-typeset details.info > summary::before { + color: #00b8d4; + content: "\E88E"; } + .md-typeset .admonition.hint, .md-typeset details.hint, .md-typeset .admonition.important, .md-typeset details.important, .md-typeset .admonition.tip, .md-typeset details.tip { + border-left-color: #00bfa5; } + [dir="rtl"] .md-typeset .admonition.hint, [dir="rtl"] .md-typeset details.hint, [dir="rtl"] .md-typeset .admonition.important, [dir="rtl"] .md-typeset details.important, [dir="rtl"] .md-typeset .admonition.tip, [dir="rtl"] .md-typeset details.tip { + border-right-color: #00bfa5; } + .md-typeset .admonition.hint > .admonition-title, .md-typeset details.hint > .admonition-title, .md-typeset .admonition.important > .admonition-title, .md-typeset details.important > .admonition-title, .md-typeset .admonition.hint > summary, .md-typeset details.hint > summary, .md-typeset .admonition.important > summary, .md-typeset details.important > summary, .md-typeset .admonition.tip > .admonition-title, .md-typeset details.tip > .admonition-title, .md-typeset .admonition.tip > summary, .md-typeset details.tip > summary { + border-bottom-color: 0.1rem solid rgba(0, 191, 165, 0.1); + background-color: rgba(0, 191, 165, 0.1); } + .md-typeset .admonition.hint > .admonition-title::before, .md-typeset details.hint > .admonition-title::before, .md-typeset .admonition.important > .admonition-title::before, .md-typeset details.important > .admonition-title::before, .md-typeset .admonition.hint > summary::before, .md-typeset details.hint > summary::before, .md-typeset .admonition.important > summary::before, .md-typeset details.important > summary::before, .md-typeset .admonition.tip > .admonition-title::before, .md-typeset details.tip > .admonition-title::before, .md-typeset .admonition.tip > summary::before, .md-typeset details.tip > summary::before { + color: #00bfa5; + content: "\E80E"; } + .md-typeset .admonition.check, .md-typeset details.check, .md-typeset .admonition.done, .md-typeset details.done, .md-typeset .admonition.success, .md-typeset details.success { + border-left-color: #00c853; } + [dir="rtl"] .md-typeset .admonition.check, [dir="rtl"] .md-typeset details.check, [dir="rtl"] .md-typeset .admonition.done, [dir="rtl"] .md-typeset details.done, [dir="rtl"] .md-typeset .admonition.success, [dir="rtl"] .md-typeset details.success { + border-right-color: #00c853; } + .md-typeset .admonition.check > .admonition-title, .md-typeset details.check > .admonition-title, .md-typeset .admonition.done > .admonition-title, .md-typeset details.done > .admonition-title, .md-typeset .admonition.check > summary, .md-typeset details.check > summary, .md-typeset .admonition.done > summary, .md-typeset details.done > summary, .md-typeset .admonition.success > .admonition-title, .md-typeset details.success > .admonition-title, .md-typeset .admonition.success > summary, .md-typeset details.success > summary { + border-bottom-color: 0.1rem solid rgba(0, 200, 83, 0.1); + background-color: rgba(0, 200, 83, 0.1); } + .md-typeset .admonition.check > .admonition-title::before, .md-typeset details.check > .admonition-title::before, .md-typeset .admonition.done > .admonition-title::before, .md-typeset details.done > .admonition-title::before, .md-typeset .admonition.check > summary::before, .md-typeset details.check > summary::before, .md-typeset .admonition.done > summary::before, .md-typeset details.done > summary::before, .md-typeset .admonition.success > .admonition-title::before, .md-typeset details.success > .admonition-title::before, .md-typeset .admonition.success > summary::before, .md-typeset details.success > summary::before { + color: #00c853; + content: "\E876"; } + .md-typeset .admonition.help, .md-typeset details.help, .md-typeset .admonition.faq, .md-typeset details.faq, .md-typeset .admonition.question, .md-typeset details.question { + border-left-color: #64dd17; } + [dir="rtl"] .md-typeset .admonition.help, [dir="rtl"] .md-typeset details.help, [dir="rtl"] .md-typeset .admonition.faq, [dir="rtl"] .md-typeset details.faq, [dir="rtl"] .md-typeset .admonition.question, [dir="rtl"] .md-typeset details.question { + border-right-color: #64dd17; } + .md-typeset .admonition.help > .admonition-title, .md-typeset details.help > .admonition-title, .md-typeset .admonition.faq > .admonition-title, .md-typeset details.faq > .admonition-title, .md-typeset .admonition.help > summary, .md-typeset details.help > summary, .md-typeset .admonition.faq > summary, .md-typeset details.faq > summary, .md-typeset .admonition.question > .admonition-title, .md-typeset details.question > .admonition-title, .md-typeset .admonition.question > summary, .md-typeset details.question > summary { + border-bottom-color: 0.1rem solid rgba(100, 221, 23, 0.1); + background-color: rgba(100, 221, 23, 0.1); } + .md-typeset .admonition.help > .admonition-title::before, .md-typeset details.help > .admonition-title::before, .md-typeset .admonition.faq > .admonition-title::before, .md-typeset details.faq > .admonition-title::before, .md-typeset .admonition.help > summary::before, .md-typeset details.help > summary::before, .md-typeset .admonition.faq > summary::before, .md-typeset details.faq > summary::before, .md-typeset .admonition.question > .admonition-title::before, .md-typeset details.question > .admonition-title::before, .md-typeset .admonition.question > summary::before, .md-typeset details.question > summary::before { + color: #64dd17; + content: "\E887"; } + .md-typeset .admonition.caution, .md-typeset details.caution, .md-typeset .admonition.attention, .md-typeset details.attention, .md-typeset .admonition.warning, .md-typeset details.warning { + border-left-color: #ff9100; } + [dir="rtl"] .md-typeset .admonition.caution, [dir="rtl"] .md-typeset details.caution, [dir="rtl"] .md-typeset .admonition.attention, [dir="rtl"] .md-typeset details.attention, [dir="rtl"] .md-typeset .admonition.warning, [dir="rtl"] .md-typeset details.warning { + border-right-color: #ff9100; } + .md-typeset .admonition.caution > .admonition-title, .md-typeset details.caution > .admonition-title, .md-typeset .admonition.attention > .admonition-title, .md-typeset details.attention > .admonition-title, .md-typeset .admonition.caution > summary, .md-typeset details.caution > summary, .md-typeset .admonition.attention > summary, .md-typeset details.attention > summary, .md-typeset .admonition.warning > .admonition-title, .md-typeset details.warning > .admonition-title, .md-typeset .admonition.warning > summary, .md-typeset details.warning > summary { + border-bottom-color: 0.1rem solid rgba(255, 145, 0, 0.1); + background-color: rgba(255, 145, 0, 0.1); } + .md-typeset .admonition.caution > .admonition-title::before, .md-typeset details.caution > .admonition-title::before, .md-typeset .admonition.attention > .admonition-title::before, .md-typeset details.attention > .admonition-title::before, .md-typeset .admonition.caution > summary::before, .md-typeset details.caution > summary::before, .md-typeset .admonition.attention > summary::before, .md-typeset details.attention > summary::before, .md-typeset .admonition.warning > .admonition-title::before, .md-typeset details.warning > .admonition-title::before, .md-typeset .admonition.warning > summary::before, .md-typeset details.warning > summary::before { + color: #ff9100; + content: "\E002"; } + .md-typeset .admonition.fail, .md-typeset details.fail, .md-typeset .admonition.missing, .md-typeset details.missing, .md-typeset .admonition.failure, .md-typeset details.failure { + border-left-color: #ff5252; } + [dir="rtl"] .md-typeset .admonition.fail, [dir="rtl"] .md-typeset details.fail, [dir="rtl"] .md-typeset .admonition.missing, [dir="rtl"] .md-typeset details.missing, [dir="rtl"] .md-typeset .admonition.failure, [dir="rtl"] .md-typeset details.failure { + border-right-color: #ff5252; } + .md-typeset .admonition.fail > .admonition-title, .md-typeset details.fail > .admonition-title, .md-typeset .admonition.missing > .admonition-title, .md-typeset details.missing > .admonition-title, .md-typeset .admonition.fail > summary, .md-typeset details.fail > summary, .md-typeset .admonition.missing > summary, .md-typeset details.missing > summary, .md-typeset .admonition.failure > .admonition-title, .md-typeset details.failure > .admonition-title, .md-typeset .admonition.failure > summary, .md-typeset details.failure > summary { + border-bottom-color: 0.1rem solid rgba(255, 82, 82, 0.1); + background-color: rgba(255, 82, 82, 0.1); } + .md-typeset .admonition.fail > .admonition-title::before, .md-typeset details.fail > .admonition-title::before, .md-typeset .admonition.missing > .admonition-title::before, .md-typeset details.missing > .admonition-title::before, .md-typeset .admonition.fail > summary::before, .md-typeset details.fail > summary::before, .md-typeset .admonition.missing > summary::before, .md-typeset details.missing > summary::before, .md-typeset .admonition.failure > .admonition-title::before, .md-typeset details.failure > .admonition-title::before, .md-typeset .admonition.failure > summary::before, .md-typeset details.failure > summary::before { + color: #ff5252; + content: "\E14C"; } + .md-typeset .admonition.error, .md-typeset details.error, .md-typeset .admonition.danger, .md-typeset details.danger { + border-left-color: #ff1744; } + [dir="rtl"] .md-typeset .admonition.error, [dir="rtl"] .md-typeset details.error, [dir="rtl"] .md-typeset .admonition.danger, [dir="rtl"] .md-typeset details.danger { + border-right-color: #ff1744; } + .md-typeset .admonition.error > .admonition-title, .md-typeset details.error > .admonition-title, .md-typeset .admonition.error > summary, .md-typeset details.error > summary, .md-typeset .admonition.danger > .admonition-title, .md-typeset details.danger > .admonition-title, .md-typeset .admonition.danger > summary, .md-typeset details.danger > summary { + border-bottom-color: 0.1rem solid rgba(255, 23, 68, 0.1); + background-color: rgba(255, 23, 68, 0.1); } + .md-typeset .admonition.error > .admonition-title::before, .md-typeset details.error > .admonition-title::before, .md-typeset .admonition.error > summary::before, .md-typeset details.error > summary::before, .md-typeset .admonition.danger > .admonition-title::before, .md-typeset details.danger > .admonition-title::before, .md-typeset .admonition.danger > summary::before, .md-typeset details.danger > summary::before { + color: #ff1744; + content: "\E3E7"; } + .md-typeset .admonition.bug, .md-typeset details.bug { + border-left-color: #f50057; } + [dir="rtl"] .md-typeset .admonition.bug, [dir="rtl"] .md-typeset details.bug { + border-right-color: #f50057; } + .md-typeset .admonition.bug > .admonition-title, .md-typeset details.bug > .admonition-title, .md-typeset .admonition.bug > summary, .md-typeset details.bug > summary { + border-bottom-color: 0.1rem solid rgba(245, 0, 87, 0.1); + background-color: rgba(245, 0, 87, 0.1); } + .md-typeset .admonition.bug > .admonition-title::before, .md-typeset details.bug > .admonition-title::before, .md-typeset .admonition.bug > summary::before, .md-typeset details.bug > summary::before { + color: #f50057; + content: "\E868"; } + .md-typeset .admonition.example, .md-typeset details.example { + border-left-color: #651fff; } + [dir="rtl"] .md-typeset .admonition.example, [dir="rtl"] .md-typeset details.example { + border-right-color: #651fff; } + .md-typeset .admonition.example > .admonition-title, .md-typeset details.example > .admonition-title, .md-typeset .admonition.example > summary, .md-typeset details.example > summary { + border-bottom-color: 0.1rem solid rgba(101, 31, 255, 0.1); + background-color: rgba(101, 31, 255, 0.1); } + .md-typeset .admonition.example > .admonition-title::before, .md-typeset details.example > .admonition-title::before, .md-typeset .admonition.example > summary::before, .md-typeset details.example > summary::before { + color: #651fff; + content: "\E242"; } + .md-typeset .admonition.cite, .md-typeset details.cite, .md-typeset .admonition.quote, .md-typeset details.quote { + border-left-color: #9e9e9e; } + [dir="rtl"] .md-typeset .admonition.cite, [dir="rtl"] .md-typeset details.cite, [dir="rtl"] .md-typeset .admonition.quote, [dir="rtl"] .md-typeset details.quote { + border-right-color: #9e9e9e; } + .md-typeset .admonition.cite > .admonition-title, .md-typeset details.cite > .admonition-title, .md-typeset .admonition.cite > summary, .md-typeset details.cite > summary, .md-typeset .admonition.quote > .admonition-title, .md-typeset details.quote > .admonition-title, .md-typeset .admonition.quote > summary, .md-typeset details.quote > summary { + border-bottom-color: 0.1rem solid rgba(158, 158, 158, 0.1); + background-color: rgba(158, 158, 158, 0.1); } + .md-typeset .admonition.cite > .admonition-title::before, .md-typeset details.cite > .admonition-title::before, .md-typeset .admonition.cite > summary::before, .md-typeset details.cite > summary::before, .md-typeset .admonition.quote > .admonition-title::before, .md-typeset details.quote > .admonition-title::before, .md-typeset .admonition.quote > summary::before, .md-typeset details.quote > summary::before { + color: #9e9e9e; + content: "\E244"; } + +.codehilite .o, .md-typeset .highlight .o { + color: inherit; } + +.codehilite .ow, .md-typeset .highlight .ow { + color: inherit; } + +.codehilite .ge, .md-typeset .highlight .ge { + color: #000000; } + +.codehilite .gr, .md-typeset .highlight .gr { + color: #AA0000; } + +.codehilite .gh, .md-typeset .highlight .gh { + color: #999999; } + +.codehilite .go, .md-typeset .highlight .go { + color: #888888; } + +.codehilite .gp, .md-typeset .highlight .gp { + color: #555555; } + +.codehilite .gs, .md-typeset .highlight .gs { + color: inherit; } + +.codehilite .gu, .md-typeset .highlight .gu { + color: #AAAAAA; } + +.codehilite .gt, .md-typeset .highlight .gt { + color: #AA0000; } + +.codehilite .gd, .md-typeset .highlight .gd { + background-color: #FFDDDD; } + +.codehilite .gi, .md-typeset .highlight .gi { + background-color: #DDFFDD; } + +.codehilite .k, .md-typeset .highlight .k { + color: #3B78E7; } + +.codehilite .kc, .md-typeset .highlight .kc { + color: #A71D5D; } + +.codehilite .kd, .md-typeset .highlight .kd { + color: #3B78E7; } + +.codehilite .kn, .md-typeset .highlight .kn { + color: #3B78E7; } + +.codehilite .kp, .md-typeset .highlight .kp { + color: #A71D5D; } + +.codehilite .kr, .md-typeset .highlight .kr { + color: #3E61A2; } + +.codehilite .kt, .md-typeset .highlight .kt { + color: #3E61A2; } + +.codehilite .c, .md-typeset .highlight .c { + color: #999999; } + +.codehilite .cm, .md-typeset .highlight .cm { + color: #999999; } + +.codehilite .cp, .md-typeset .highlight .cp { + color: #666666; } + +.codehilite .c1, .md-typeset .highlight .c1 { + color: #999999; } + +.codehilite .ch, .md-typeset .highlight .ch { + color: #999999; } + +.codehilite .cs, .md-typeset .highlight .cs { + color: #999999; } + +.codehilite .na, .md-typeset .highlight .na { + color: #C2185B; } + +.codehilite .nb, .md-typeset .highlight .nb { + color: #C2185B; } + +.codehilite .bp, .md-typeset .highlight .bp { + color: #3E61A2; } + +.codehilite .nc, .md-typeset .highlight .nc { + color: #C2185B; } + +.codehilite .no, .md-typeset .highlight .no { + color: #3E61A2; } + +.codehilite .nd, .md-typeset .highlight .nd { + color: #666666; } + +.codehilite .ni, .md-typeset .highlight .ni { + color: #666666; } + +.codehilite .ne, .md-typeset .highlight .ne { + color: #C2185B; } + +.codehilite .nf, .md-typeset .highlight .nf { + color: #C2185B; } + +.codehilite .nl, .md-typeset .highlight .nl { + color: #3B5179; } + +.codehilite .nn, .md-typeset .highlight .nn { + color: #EC407A; } + +.codehilite .nt, .md-typeset .highlight .nt { + color: #3B78E7; } + +.codehilite .nv, .md-typeset .highlight .nv { + color: #3E61A2; } + +.codehilite .vc, .md-typeset .highlight .vc { + color: #3E61A2; } + +.codehilite .vg, .md-typeset .highlight .vg { + color: #3E61A2; } + +.codehilite .vi, .md-typeset .highlight .vi { + color: #3E61A2; } + +.codehilite .nx, .md-typeset .highlight .nx { + color: #EC407A; } + +.codehilite .m, .md-typeset .highlight .m { + color: #E74C3C; } + +.codehilite .mf, .md-typeset .highlight .mf { + color: #E74C3C; } + +.codehilite .mh, .md-typeset .highlight .mh { + color: #E74C3C; } + +.codehilite .mi, .md-typeset .highlight .mi { + color: #E74C3C; } + +.codehilite .il, .md-typeset .highlight .il { + color: #E74C3C; } + +.codehilite .mo, .md-typeset .highlight .mo { + color: #E74C3C; } + +.codehilite .s, .md-typeset .highlight .s { + color: #0D904F; } + +.codehilite .sb, .md-typeset .highlight .sb { + color: #0D904F; } + +.codehilite .sc, .md-typeset .highlight .sc { + color: #0D904F; } + +.codehilite .sd, .md-typeset .highlight .sd { + color: #999999; } + +.codehilite .s2, .md-typeset .highlight .s2 { + color: #0D904F; } + +.codehilite .se, .md-typeset .highlight .se { + color: #183691; } + +.codehilite .sh, .md-typeset .highlight .sh { + color: #183691; } + +.codehilite .si, .md-typeset .highlight .si { + color: #183691; } + +.codehilite .sx, .md-typeset .highlight .sx { + color: #183691; } + +.codehilite .sr, .md-typeset .highlight .sr { + color: #009926; } + +.codehilite .s1, .md-typeset .highlight .s1 { + color: #0D904F; } + +.codehilite .ss, .md-typeset .highlight .ss { + color: #0D904F; } + +.codehilite .err, .md-typeset .highlight .err { + color: #A61717; } + +.codehilite .w, .md-typeset .highlight .w { + color: transparent; } + +.codehilite .hll, .md-typeset .highlight .hll { + display: block; + margin: 0 -1.2rem; + padding: 0 1.2rem; + background-color: rgba(255, 235, 59, 0.5); } + +.md-typeset .codehilite, .md-typeset .highlight { + position: relative; + margin: 1em 0; + padding: 0; + border-radius: 0.2rem; + background-color: rgba(236, 236, 236, 0.5); + color: #37474F; + line-height: 1.4; + -webkit-overflow-scrolling: touch; } + .md-typeset .codehilite pre, .md-typeset .highlight pre, + .md-typeset .codehilite code, .md-typeset .highlight code { + display: block; + margin: 0; + padding: 1.05rem 1.2rem; + background-color: transparent; + overflow: auto; + vertical-align: top; } + .md-typeset .codehilite pre::-webkit-scrollbar, .md-typeset .highlight pre::-webkit-scrollbar, + .md-typeset .codehilite code::-webkit-scrollbar, .md-typeset .highlight code::-webkit-scrollbar { + width: 0.4rem; + height: 0.4rem; } + .md-typeset .codehilite pre::-webkit-scrollbar-thumb, .md-typeset .highlight pre::-webkit-scrollbar-thumb, + .md-typeset .codehilite code::-webkit-scrollbar-thumb, .md-typeset .highlight code::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.26); } + .md-typeset .codehilite pre::-webkit-scrollbar-thumb:hover, .md-typeset .highlight pre::-webkit-scrollbar-thumb:hover, + .md-typeset .codehilite code::-webkit-scrollbar-thumb:hover, .md-typeset .highlight code::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + +.md-typeset pre.codehilite, .md-typeset pre.highlight { + overflow: visible; } + .md-typeset pre.codehilite code, .md-typeset pre.highlight code { + display: block; + padding: 1.05rem 1.2rem; + overflow: auto; } + +.md-typeset .codehilitetable, .md-typeset .highlighttable { + display: block; + margin: 1em 0; + border-radius: 0.2em; + font-size: 1.6rem; + overflow: hidden; } + .md-typeset .codehilitetable tbody, .md-typeset .highlighttable tbody, + .md-typeset .codehilitetable td, .md-typeset .highlighttable td { + display: block; + padding: 0; } + .md-typeset .codehilitetable tr, .md-typeset .highlighttable tr { + display: flex; } + .md-typeset .codehilitetable .codehilite, .md-typeset .highlighttable .codehilite, .md-typeset .codehilitetable .highlight, .md-typeset .highlighttable .highlight, + .md-typeset .codehilitetable .linenodiv, .md-typeset .highlighttable .linenodiv { + margin: 0; + border-radius: 0; } + .md-typeset .codehilitetable .linenodiv, .md-typeset .highlighttable .linenodiv { + padding: 1.05rem 1.2rem; } + .md-typeset .codehilitetable .linenos, .md-typeset .highlighttable .linenos { + background-color: rgba(0, 0, 0, 0.07); + color: rgba(0, 0, 0, 0.26); + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } + .md-typeset .codehilitetable .linenos pre, .md-typeset .highlighttable .linenos pre { + margin: 0; + padding: 0; + background-color: transparent; + color: inherit; + text-align: right; } + .md-typeset .codehilitetable .code, .md-typeset .highlighttable .code { + flex: 1; + overflow: hidden; } + +.md-typeset > .codehilitetable, .md-typeset > .highlighttable { + box-shadow: none; } + +.md-typeset [id^="fnref:"] { + display: inline-block; } + .md-typeset [id^="fnref:"]:target { + margin-top: -7.6rem; + padding-top: 7.6rem; + pointer-events: none; } + +.md-typeset [id^="fn:"]::before { + display: none; + height: 0; + content: ""; } + +.md-typeset [id^="fn:"]:target::before { + display: block; + margin-top: -7rem; + padding-top: 7rem; + pointer-events: none; } + +.md-typeset .footnote { + color: rgba(0, 0, 0, 0.54); + font-size: 1.28rem; } + .md-typeset .footnote ol { + margin-left: 0; } + .md-typeset .footnote li { + transition: color 0.25s; } + .md-typeset .footnote li:target { + color: rgba(0, 0, 0, 0.87); } + .md-typeset .footnote li :first-child { + margin-top: 0; } + .md-typeset .footnote li:hover .footnote-backref, + .md-typeset .footnote li:target .footnote-backref { + -webkit-transform: translateX(0); + transform: translateX(0); + opacity: 1; } + .md-typeset .footnote li:hover .footnote-backref:hover, + .md-typeset .footnote li:target .footnote-backref { + color: #536dfe; } + +.md-typeset .footnote-ref { + display: inline-block; + pointer-events: initial; } + .md-typeset .footnote-ref::before { + display: inline; + margin: 0 0.2em; + border-left: 0.1rem solid rgba(0, 0, 0, 0.26); + font-size: 1.25em; + content: ""; + vertical-align: -0.5rem; } + +.md-typeset .footnote-backref { + display: inline-block; + -webkit-transform: translateX(0.5rem); + transform: translateX(0.5rem); + transition: color 0.25s, opacity 0.125s 0.125s, -webkit-transform 0.25s 0.125s; + transition: transform 0.25s 0.125s, color 0.25s, opacity 0.125s 0.125s; + transition: transform 0.25s 0.125s, color 0.25s, opacity 0.125s 0.125s, -webkit-transform 0.25s 0.125s; + color: rgba(0, 0, 0, 0.26); + font-size: 0; + opacity: 0; + vertical-align: text-bottom; } + [dir="rtl"] .md-typeset .footnote-backref { + -webkit-transform: translateX(-0.5rem); + transform: translateX(-0.5rem); } + .md-typeset .footnote-backref::before { + display: inline-block; + font-size: 1.6rem; + content: "\E31B"; } + [dir="rtl"] .md-typeset .footnote-backref::before { + -webkit-transform: scaleX(-1); + transform: scaleX(-1); } + +.md-typeset .headerlink { + display: inline-block; + margin-left: 1rem; + -webkit-transform: translate(0, 0.5rem); + transform: translate(0, 0.5rem); + transition: color 0.25s, opacity 0.125s 0.25s, -webkit-transform 0.25s 0.25s; + transition: transform 0.25s 0.25s, color 0.25s, opacity 0.125s 0.25s; + transition: transform 0.25s 0.25s, color 0.25s, opacity 0.125s 0.25s, -webkit-transform 0.25s 0.25s; + opacity: 0; } + [dir="rtl"] .md-typeset .headerlink { + margin-right: 1rem; + margin-left: initial; } + html body .md-typeset .headerlink { + color: rgba(0, 0, 0, 0.26); } + +.md-typeset h1[id]::before { + display: block; + margin-top: -0.9rem; + padding-top: 0.9rem; + content: ""; } + +.md-typeset h1[id]:target::before { + margin-top: -6.9rem; + padding-top: 6.9rem; } + +.md-typeset h1[id]:hover .headerlink, +.md-typeset h1[id]:target .headerlink, +.md-typeset h1[id] .headerlink:focus { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + opacity: 1; } + +.md-typeset h1[id]:hover .headerlink:hover, +.md-typeset h1[id]:target .headerlink, +.md-typeset h1[id] .headerlink:focus { + color: #536dfe; } + +.md-typeset h2[id]::before { + display: block; + margin-top: -0.8rem; + padding-top: 0.8rem; + content: ""; } + +.md-typeset h2[id]:target::before { + margin-top: -6.8rem; + padding-top: 6.8rem; } + +.md-typeset h2[id]:hover .headerlink, +.md-typeset h2[id]:target .headerlink, +.md-typeset h2[id] .headerlink:focus { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + opacity: 1; } + +.md-typeset h2[id]:hover .headerlink:hover, +.md-typeset h2[id]:target .headerlink, +.md-typeset h2[id] .headerlink:focus { + color: #536dfe; } + +.md-typeset h3[id]::before { + display: block; + margin-top: -0.9rem; + padding-top: 0.9rem; + content: ""; } + +.md-typeset h3[id]:target::before { + margin-top: -6.9rem; + padding-top: 6.9rem; } + +.md-typeset h3[id]:hover .headerlink, +.md-typeset h3[id]:target .headerlink, +.md-typeset h3[id] .headerlink:focus { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + opacity: 1; } + +.md-typeset h3[id]:hover .headerlink:hover, +.md-typeset h3[id]:target .headerlink, +.md-typeset h3[id] .headerlink:focus { + color: #536dfe; } + +.md-typeset h4[id]::before { + display: block; + margin-top: -0.9rem; + padding-top: 0.9rem; + content: ""; } + +.md-typeset h4[id]:target::before { + margin-top: -6.9rem; + padding-top: 6.9rem; } + +.md-typeset h4[id]:hover .headerlink, +.md-typeset h4[id]:target .headerlink, +.md-typeset h4[id] .headerlink:focus { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + opacity: 1; } + +.md-typeset h4[id]:hover .headerlink:hover, +.md-typeset h4[id]:target .headerlink, +.md-typeset h4[id] .headerlink:focus { + color: #536dfe; } + +.md-typeset h5[id]::before { + display: block; + margin-top: -1.1rem; + padding-top: 1.1rem; + content: ""; } + +.md-typeset h5[id]:target::before { + margin-top: -7.1rem; + padding-top: 7.1rem; } + +.md-typeset h5[id]:hover .headerlink, +.md-typeset h5[id]:target .headerlink, +.md-typeset h5[id] .headerlink:focus { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + opacity: 1; } + +.md-typeset h5[id]:hover .headerlink:hover, +.md-typeset h5[id]:target .headerlink, +.md-typeset h5[id] .headerlink:focus { + color: #536dfe; } + +.md-typeset h6[id]::before { + display: block; + margin-top: -1.1rem; + padding-top: 1.1rem; + content: ""; } + +.md-typeset h6[id]:target::before { + margin-top: -7.1rem; + padding-top: 7.1rem; } + +.md-typeset h6[id]:hover .headerlink, +.md-typeset h6[id]:target .headerlink, +.md-typeset h6[id] .headerlink:focus { + -webkit-transform: translate(0, 0); + transform: translate(0, 0); + opacity: 1; } + +.md-typeset h6[id]:hover .headerlink:hover, +.md-typeset h6[id]:target .headerlink, +.md-typeset h6[id] .headerlink:focus { + color: #536dfe; } + +.md-typeset .MJXc-display { + margin: 0.75em 0; + padding: 0.75em 0; + overflow: auto; + -webkit-overflow-scrolling: touch; } + +.md-typeset .MathJax_CHTML { + outline: 0; } + +.md-typeset del.critic, +.md-typeset ins.critic, +.md-typeset .critic.comment { + margin: 0 0.25em; + padding: 0.0625em 0; + border-radius: 0.2rem; + -webkit-box-decoration-break: clone; + box-decoration-break: clone; } + +.md-typeset del.critic { + background-color: #FFDDDD; + box-shadow: 0.25em 0 0 #FFDDDD, -0.25em 0 0 #FFDDDD; } + +.md-typeset ins.critic { + background-color: #DDFFDD; + box-shadow: 0.25em 0 0 #DDFFDD, -0.25em 0 0 #DDFFDD; } + +.md-typeset .critic.comment { + background-color: rgba(236, 236, 236, 0.5); + color: #37474F; + box-shadow: 0.25em 0 0 rgba(236, 236, 236, 0.5), -0.25em 0 0 rgba(236, 236, 236, 0.5); } + .md-typeset .critic.comment::before { + padding-right: 0.125em; + color: rgba(0, 0, 0, 0.26); + content: "\E0B7"; + vertical-align: -0.125em; } + +.md-typeset .critic.block { + display: block; + margin: 1em 0; + padding-right: 1.6rem; + padding-left: 1.6rem; + box-shadow: none; } + .md-typeset .critic.block :first-child { + margin-top: 0.5em; } + .md-typeset .critic.block :last-child { + margin-bottom: 0.5em; } + +.md-typeset details { + display: block; + padding-top: 0; } + .md-typeset details[open] > summary::after { + -webkit-transform: rotate(180deg); + transform: rotate(180deg); } + .md-typeset details:not([open]) { + padding-bottom: 0; } + .md-typeset details:not([open]) > summary { + border-bottom: none; } + .md-typeset details summary { + padding-right: 4rem; } + [dir="rtl"] .md-typeset details summary { + padding-left: 4rem; } + .no-details .md-typeset details:not([open]) > * { + display: none; } + .no-details .md-typeset details:not([open]) summary { + display: block; } + +.md-typeset summary { + display: block; + outline: none; + cursor: pointer; } + .md-typeset summary::-webkit-details-marker { + display: none; } + .md-typeset summary::after { + position: absolute; + top: 0.8rem; + right: 1.2rem; + color: rgba(0, 0, 0, 0.26); + font-size: 2rem; + content: "\E313"; } + [dir="rtl"] .md-typeset summary::after { + right: initial; + left: 1.2rem; } + +.md-typeset .emojione { + width: 2rem; + vertical-align: text-top; } + +.md-typeset code.codehilite, .md-typeset code.highlight { + margin: 0 0.29412em; + padding: 0.07353em 0; } + +.md-typeset .superfences-content { + display: none; + order: 99; + width: 100%; + background-color: white; } + .md-typeset .superfences-content > * { + margin: 0; + border-radius: 0; } + +.md-typeset .superfences-tabs { + display: flex; + position: relative; + flex-wrap: wrap; + margin: 1em 0; + border: 0.1rem solid rgba(0, 0, 0, 0.07); + border-radius: 0.2em; } + .md-typeset .superfences-tabs > input { + display: none; } + .md-typeset .superfences-tabs > input:checked + label { + font-weight: 700; } + .md-typeset .superfences-tabs > input:checked + label + .superfences-content { + display: block; } + .md-typeset .superfences-tabs > label { + width: auto; + padding: 1.2rem 1.2rem; + transition: color 0.125s; + font-size: 1.28rem; + cursor: pointer; } + html .md-typeset .superfences-tabs > label:hover { + color: #536dfe; } + +.md-typeset .task-list-item { + position: relative; + list-style-type: none; } + .md-typeset .task-list-item [type="checkbox"] { + position: absolute; + top: 0.45em; + left: -2em; } + [dir="rtl"] .md-typeset .task-list-item [type="checkbox"] { + right: -2em; + left: initial; } + +.md-typeset .task-list-control .task-list-indicator::before { + position: absolute; + top: 0.15em; + left: -1.25em; + color: rgba(0, 0, 0, 0.26); + font-size: 1.25em; + content: "\E835"; + vertical-align: -0.25em; } + [dir="rtl"] .md-typeset .task-list-control .task-list-indicator::before { + right: -1.25em; + left: initial; } + +.md-typeset .task-list-control [type="checkbox"]:checked + .task-list-indicator::before { + content: "\E834"; } + +.md-typeset .task-list-control [type="checkbox"] { + opacity: 0; + z-index: -1; } + +@media print { + .md-typeset a::after { + color: rgba(0, 0, 0, 0.54); + content: " [" attr(href) "]"; } + .md-typeset code, + .md-typeset pre { + white-space: pre-wrap; } + .md-typeset code { + box-shadow: none; + -webkit-box-decoration-break: initial; + box-decoration-break: initial; } + .md-clipboard { + display: none; } + .md-content__icon { + display: none; } + .md-header { + display: none; } + .md-footer { + display: none; } + .md-sidebar { + display: none; } + .md-tabs { + display: none; } + .md-typeset .headerlink { + display: none; } } + +@media only screen and (max-width: 44.9375em) { + .md-typeset pre { + margin: 1em -1.6rem; + border-radius: 0; } + .md-typeset pre > code { + padding: 1.05rem 1.6rem; } + .md-footer-nav__link--prev .md-footer-nav__title { + display: none; } + .md-search-result__teaser { + max-height: 5rem; + -webkit-line-clamp: 3; } + .codehilite .hll, .md-typeset .highlight .hll { + margin: 0 -1.6rem; + padding: 0 1.6rem; } + .md-typeset > .codehilite, .md-typeset > .highlight { + margin: 1em -1.6rem; + border-radius: 0; } + .md-typeset > .codehilite pre, .md-typeset > .highlight pre, + .md-typeset > .codehilite code, + .md-typeset > .highlight code { + padding: 1.05rem 1.6rem; } + .md-typeset > .codehilitetable, .md-typeset > .highlighttable { + margin: 1em -1.6rem; + border-radius: 0; } + .md-typeset > .codehilitetable .codehilite > pre, .md-typeset > .highlighttable .codehilite > pre, .md-typeset > .codehilitetable .highlight > pre, .md-typeset > .highlighttable .highlight > pre, + .md-typeset > .codehilitetable .codehilite > code, + .md-typeset > .highlighttable .codehilite > code, .md-typeset > .codehilitetable .highlight > code, .md-typeset > .highlighttable .highlight > code, + .md-typeset > .codehilitetable .linenodiv, + .md-typeset > .highlighttable .linenodiv { + padding: 1rem 1.6rem; } + .md-typeset > p > .MJXc-display { + margin: 0.75em -1.6rem; + padding: 0.25em 1.6rem; } + .md-typeset > .superfences-tabs { + margin: 1em -1.6rem; + border: 0; + border-top: 0.1rem solid rgba(0, 0, 0, 0.07); + border-radius: 0; } + .md-typeset > .superfences-tabs pre, + .md-typeset > .superfences-tabs code { + padding: 1.05rem 1.6rem; } } + +@media only screen and (min-width: 100em) { + html { + font-size: 68.75%; } } + +@media only screen and (min-width: 125em) { + html { + font-size: 75%; } } + +@media only screen and (max-width: 59.9375em) { + body[data-md-state="lock"] { + overflow: hidden; } + .ios body[data-md-state="lock"] .md-container { + display: none; } + html .md-nav__link[for="__toc"] { + display: block; + padding-right: 4.8rem; } + html .md-nav__link[for="__toc"]::after { + color: inherit; + content: "\E8DE"; } + html .md-nav__link[for="__toc"] + .md-nav__link { + display: none; } + html .md-nav__link[for="__toc"] ~ .md-nav { + display: flex; } + html [dir="rtl"] .md-nav__link { + padding-right: 1.6rem; + padding-left: 4.8rem; } + .md-nav__source { + display: block; + padding: 0 0.4rem; + background-color: rgba(50, 64, 144, 0.9675); + color: white; } + .md-search__overlay { + position: absolute; + top: 0.4rem; + left: 0.4rem; + width: 3.6rem; + height: 3.6rem; + -webkit-transform-origin: center; + transform-origin: center; + transition: opacity 0.2s 0.2s, -webkit-transform 0.3s 0.1s; + transition: transform 0.3s 0.1s, opacity 0.2s 0.2s; + transition: transform 0.3s 0.1s, opacity 0.2s 0.2s, -webkit-transform 0.3s 0.1s; + border-radius: 2rem; + background-color: white; + overflow: hidden; + pointer-events: none; } + [dir="rtl"] .md-search__overlay { + right: 0.4rem; + left: initial; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__overlay { + transition: opacity 0.1s, -webkit-transform 0.4s; + transition: transform 0.4s, opacity 0.1s; + transition: transform 0.4s, opacity 0.1s, -webkit-transform 0.4s; + opacity: 1; } + .md-search__inner { + position: fixed; + top: 0; + left: 100%; + width: 100%; + height: 100%; + -webkit-transform: translateX(5%); + transform: translateX(5%); + transition: right 0s 0.3s, left 0s 0.3s, opacity 0.15s 0.15s, -webkit-transform 0.15s 0.15s cubic-bezier(0.4, 0, 0.2, 1); + transition: right 0s 0.3s, left 0s 0.3s, transform 0.15s 0.15s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s 0.15s; + transition: right 0s 0.3s, left 0s 0.3s, transform 0.15s 0.15s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s 0.15s, -webkit-transform 0.15s 0.15s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 0; + z-index: 2; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__inner { + left: 0; + -webkit-transform: translateX(0); + transform: translateX(0); + transition: right 0s 0s, left 0s 0s, opacity 0.15s 0.15s, -webkit-transform 0.15s 0.15s cubic-bezier(0.1, 0.7, 0.1, 1); + transition: right 0s 0s, left 0s 0s, transform 0.15s 0.15s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s 0.15s; + transition: right 0s 0s, left 0s 0s, transform 0.15s 0.15s cubic-bezier(0.1, 0.7, 0.1, 1), opacity 0.15s 0.15s, -webkit-transform 0.15s 0.15s cubic-bezier(0.1, 0.7, 0.1, 1); + opacity: 1; } + [dir="rtl"] [data-md-toggle="search"]:checked ~ .md-header .md-search__inner { + right: 0; + left: initial; } + html [dir="rtl"] .md-search__inner { + right: 100%; + left: initial; + -webkit-transform: translateX(-5%); + transform: translateX(-5%); } + .md-search__input { + width: 100%; + height: 4.8rem; + font-size: 1.8rem; } + .md-search__icon[for="__search"] { + top: 1.2rem; + left: 1.6rem; } + .md-search__icon[for="__search"][for="__search"]::before { + content: "\E5C4"; } + [dir="rtl"] .md-search__icon[for="__search"][for="__search"]::before { + content: "\E5C8"; } + .md-search__icon[type="reset"] { + top: 1.2rem; + right: 1.6rem; } + .md-search__output { + top: 4.8rem; + bottom: 0; } + .md-search-result__article--document::before { + display: none; } } + +@media only screen and (max-width: 76.1875em) { + [data-md-toggle="drawer"]:checked ~ .md-overlay { + width: 100%; + height: 100%; + transition: width 0s, height 0s, opacity 0.25s; + opacity: 1; } + .md-header-nav__button.md-icon--home, .md-header-nav__button.md-logo { + display: none; } + .md-hero__inner { + margin-top: 4.8rem; + margin-bottom: 2.4rem; } + .md-nav { + background-color: white; } + .md-nav--primary, + .md-nav--primary .md-nav { + display: flex; + position: absolute; + top: 0; + right: 0; + left: 0; + flex-direction: column; + height: 100%; + z-index: 1; } + .md-nav--primary .md-nav__title, + .md-nav--primary .md-nav__item { + font-size: 1.6rem; + line-height: 1.5; } + html .md-nav--primary .md-nav__title { + position: relative; + height: 11.2rem; + padding: 6rem 1.6rem 0.4rem; + background-color: rgba(0, 0, 0, 0.07); + color: rgba(0, 0, 0, 0.54); + font-weight: 400; + line-height: 4.8rem; + white-space: nowrap; + cursor: pointer; } + html .md-nav--primary .md-nav__title::before { + display: block; + position: absolute; + top: 0.4rem; + left: 0.4rem; + width: 4rem; + height: 4rem; + color: rgba(0, 0, 0, 0.54); } + html .md-nav--primary .md-nav__title ~ .md-nav__list { + background-color: white; + box-shadow: 0 0.1rem 0 rgba(0, 0, 0, 0.07) inset; } + html .md-nav--primary .md-nav__title ~ .md-nav__list > .md-nav__item:first-child { + border-top: 0; } + html .md-nav--primary .md-nav__title--site { + position: relative; + background-color: #3f51b5; + color: white; } + html .md-nav--primary .md-nav__title--site .md-nav__button { + display: block; + position: absolute; + top: 0.4rem; + left: 0.4rem; + width: 6.4rem; + height: 6.4rem; + font-size: 4.8rem; } + html .md-nav--primary .md-nav__title--site::before { + display: none; } + html [dir="rtl"] .md-nav--primary .md-nav__title::before { + right: 0.4rem; + left: initial; } + html [dir="rtl"] .md-nav--primary .md-nav__title--site .md-nav__button { + right: 0.4rem; + left: initial; } + .md-nav--primary .md-nav__list { + flex: 1; + overflow-y: auto; } + .md-nav--primary .md-nav__item { + padding: 0; + border-top: 0.1rem solid rgba(0, 0, 0, 0.07); } + [dir="rtl"] .md-nav--primary .md-nav__item { + padding: 0; } + .md-nav--primary .md-nav__item--nested > .md-nav__link { + padding-right: 4.8rem; } + [dir="rtl"] .md-nav--primary .md-nav__item--nested > .md-nav__link { + padding-right: 1.6rem; + padding-left: 4.8rem; } + .md-nav--primary .md-nav__item--nested > .md-nav__link::after { + content: "\E315"; } + [dir="rtl"] .md-nav--primary .md-nav__item--nested > .md-nav__link::after { + content: "\E314"; } + .md-nav--primary .md-nav__link { + position: relative; + margin-top: 0; + padding: 1.2rem 1.6rem; } + .md-nav--primary .md-nav__link::after { + position: absolute; + top: 50%; + right: 1.2rem; + margin-top: -1.2rem; + color: inherit; + font-size: 2.4rem; } + [dir="rtl"] .md-nav--primary .md-nav__link::after { + right: initial; + left: 1.2rem; } + .md-nav--primary .md-nav--secondary .md-nav__link { + position: static; } + .md-nav--primary .md-nav--secondary .md-nav { + position: static; + background-color: transparent; } + .md-nav--primary .md-nav--secondary .md-nav .md-nav__link { + padding-left: 2.8rem; } + [dir="rtl"] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link { + padding-right: 2.8rem; + padding-left: initial; } + .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link { + padding-left: 4rem; } + [dir="rtl"] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link { + padding-right: 4rem; + padding-left: initial; } + .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link { + padding-left: 5.2rem; } + [dir="rtl"] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link { + padding-right: 5.2rem; + padding-left: initial; } + .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link { + padding-left: 6.4rem; } + [dir="rtl"] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link { + padding-right: 6.4rem; + padding-left: initial; } + .md-nav__toggle ~ .md-nav { + display: flex; + -webkit-transform: translateX(100%); + transform: translateX(100%); + transition: opacity 0.125s 0.05s, -webkit-transform 0.25s cubic-bezier(0.8, 0, 0.6, 1); + transition: transform 0.25s cubic-bezier(0.8, 0, 0.6, 1), opacity 0.125s 0.05s; + transition: transform 0.25s cubic-bezier(0.8, 0, 0.6, 1), opacity 0.125s 0.05s, -webkit-transform 0.25s cubic-bezier(0.8, 0, 0.6, 1); + opacity: 0; } + [dir="rtl"] .md-nav__toggle ~ .md-nav { + -webkit-transform: translateX(-100%); + transform: translateX(-100%); } + .no-csstransforms3d .md-nav__toggle ~ .md-nav { + display: none; } + .md-nav__toggle:checked ~ .md-nav { + -webkit-transform: translateX(0); + transform: translateX(0); + transition: opacity 0.125s 0.125s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.125s 0.125s; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.125s 0.125s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + opacity: 1; } + .no-csstransforms3d .md-nav__toggle:checked ~ .md-nav { + display: flex; } + .md-sidebar--primary { + position: fixed; + top: 0; + left: -24.2rem; + width: 24.2rem; + height: 100%; + -webkit-transform: translateX(0); + transform: translateX(0); + transition: box-shadow 0.25s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s; + transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s, -webkit-transform 0.25s cubic-bezier(0.4, 0, 0.2, 1); + background-color: white; + z-index: 3; } + [dir="rtl"] .md-sidebar--primary { + right: -24.2rem; + left: initial; } + .no-csstransforms3d .md-sidebar--primary { + display: none; } + [data-md-toggle="drawer"]:checked ~ .md-container .md-sidebar--primary { + box-shadow: 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12), 0 5px 5px -3px rgba(0, 0, 0, 0.4); + -webkit-transform: translateX(24.2rem); + transform: translateX(24.2rem); } + [dir="rtl"] [data-md-toggle="drawer"]:checked ~ .md-container .md-sidebar--primary { + -webkit-transform: translateX(-24.2rem); + transform: translateX(-24.2rem); } + .no-csstransforms3d [data-md-toggle="drawer"]:checked ~ .md-container .md-sidebar--primary { + display: block; } + .md-sidebar--primary .md-sidebar__scrollwrap { + overflow: hidden; } + .md-sidebar--primary .md-sidebar__scrollwrap { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + margin: 0; } + .md-tabs { + display: none; } } + +@media only screen and (min-width: 60em) { + .md-content { + margin-right: 24.2rem; } + [dir="rtl"] .md-content { + margin-right: initial; + margin-left: 24.2rem; } + .md-header-nav__button.md-icon--search { + display: none; } + .md-header-nav__source { + display: block; + width: 23rem; + max-width: 23rem; + margin-left: 2.8rem; + padding-right: 1.2rem; } + [dir="rtl"] .md-header-nav__source { + margin-right: 2.8rem; + margin-left: initial; + padding-right: initial; + padding-left: 1.2rem; } + .md-search { + padding: 0.4rem; } + .md-search__overlay { + position: fixed; + top: 0; + left: 0; + width: 0; + height: 0; + transition: width 0s 0.25s, height 0s 0.25s, opacity 0.25s; + background-color: rgba(0, 0, 0, 0.54); + cursor: pointer; } + [dir="rtl"] .md-search__overlay { + right: 0; + left: initial; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__overlay { + width: 100%; + height: 100%; + transition: width 0s, height 0s, opacity 0.25s; + opacity: 1; } + .md-search__inner { + position: relative; + width: 23rem; + padding: 0.2rem 0; + float: right; + transition: width 0.25s cubic-bezier(0.1, 0.7, 0.1, 1); } + [dir="rtl"] .md-search__inner { + float: left; } + .md-search__form { + border-radius: 0.2rem; } + .md-search__input { + width: 100%; + height: 3.6rem; + padding-left: 4.4rem; + transition: background-color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1), color 0.25s cubic-bezier(0.1, 0.7, 0.1, 1); + border-radius: 0.2rem; + background-color: rgba(0, 0, 0, 0.26); + color: inherit; + font-size: 1.6rem; } + [dir="rtl"] .md-search__input { + padding-right: 4.4rem; } + .md-search__input + .md-search__icon { + color: inherit; } + .md-search__input::-webkit-input-placeholder { + color: rgba(255, 255, 255, 0.7); } + .md-search__input:-ms-input-placeholder { + color: rgba(255, 255, 255, 0.7); } + .md-search__input::-ms-input-placeholder { + color: rgba(255, 255, 255, 0.7); } + .md-search__input::placeholder { + color: rgba(255, 255, 255, 0.7); } + .md-search__input:hover { + background-color: rgba(255, 255, 255, 0.12); } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input { + border-radius: 0.2rem 0.2rem 0 0; + background-color: white; + color: rgba(0, 0, 0, 0.87); + text-overflow: none; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input + .md-search__icon, [data-md-toggle="search"]:checked ~ .md-header .md-search__input::-webkit-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input + .md-search__icon, [data-md-toggle="search"]:checked ~ .md-header .md-search__input:-ms-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input + .md-search__icon, [data-md-toggle="search"]:checked ~ .md-header .md-search__input::-ms-input-placeholder { + color: rgba(0, 0, 0, 0.54); } + [data-md-toggle="search"]:checked ~ .md-header .md-search__input + .md-search__icon, [data-md-toggle="search"]:checked ~ .md-header .md-search__input::placeholder { + color: rgba(0, 0, 0, 0.54); } + .md-search__output { + top: 3.8rem; + transition: opacity 0.4s; + opacity: 0; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__output { + box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12), 0 3px 5px -1px rgba(0, 0, 0, 0.4); + opacity: 1; } + .md-search__scrollwrap { + max-height: 0; } + [data-md-toggle="search"]:checked ~ .md-header .md-search__scrollwrap { + max-height: 75vh; } + .md-search__scrollwrap::-webkit-scrollbar { + width: 0.4rem; + height: 0.4rem; } + .md-search__scrollwrap::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.26); } + .md-search__scrollwrap::-webkit-scrollbar-thumb:hover { + background-color: #536dfe; } + .md-search-result__meta { + padding-left: 4.4rem; } + [dir="rtl"] .md-search-result__meta { + padding-right: 4.4rem; + padding-left: initial; } + .md-search-result__article { + padding-left: 4.4rem; } + [dir="rtl"] .md-search-result__article { + padding-right: 4.4rem; + padding-left: 1.6rem; } + .md-sidebar--secondary { + display: block; + margin-left: 100%; + -webkit-transform: translate(-100%, 0); + transform: translate(-100%, 0); } + [dir="rtl"] .md-sidebar--secondary { + margin-right: 100%; + margin-left: initial; + -webkit-transform: translate(100%, 0); + transform: translate(100%, 0); } } + +@media only screen and (min-width: 76.25em) { + .md-content { + margin-left: 24.2rem; } + [dir="rtl"] .md-content { + margin-right: 24.2rem; } + .md-content__inner { + margin-right: 2.4rem; + margin-left: 2.4rem; } + .md-header-nav__button.md-icon--menu { + display: none; } + .md-nav[data-md-state="animate"] { + transition: max-height 0.25s cubic-bezier(0.86, 0, 0.07, 1); } + .md-nav__toggle ~ .md-nav { + max-height: 0; + overflow: hidden; } + .no-js .md-nav__toggle ~ .md-nav { + display: none; } + .md-nav__toggle:checked ~ .md-nav, .md-nav[data-md-state="expand"] { + max-height: 100%; } + .no-js .md-nav__toggle:checked ~ .md-nav, .no-js .md-nav[data-md-state="expand"] { + display: block; } + .md-nav__item--nested > .md-nav > .md-nav__title { + display: none; } + .md-nav__item--nested > .md-nav__link::after { + display: inline-block; + -webkit-transform-origin: 0.45em 0.45em; + transform-origin: 0.45em 0.45em; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + vertical-align: -0.125em; } + .js .md-nav__item--nested > .md-nav__link::after { + transition: -webkit-transform 0.4s; + transition: transform 0.4s; + transition: transform 0.4s, -webkit-transform 0.4s; } + .md-nav__item--nested .md-nav__toggle:checked ~ .md-nav__link::after { + -webkit-transform: rotateX(180deg); + transform: rotateX(180deg); } + [data-md-toggle="search"]:checked ~ .md-header .md-search__inner { + width: 68.8rem; } + .md-search__scrollwrap { + width: 68.8rem; } + .md-sidebar--secondary { + margin-left: 122rem; } + [dir="rtl"] .md-sidebar--secondary { + margin-right: 122rem; + margin-left: initial; } + .md-tabs ~ .md-main .md-nav--primary > .md-nav__list > .md-nav__item--nested { + font-size: 0; + visibility: hidden; } + .md-tabs--active ~ .md-main .md-nav--primary .md-nav__title { + display: block; + padding: 0; } + .md-tabs--active ~ .md-main .md-nav--primary .md-nav__title--site { + display: none; } + .no-js .md-tabs--active ~ .md-main .md-nav--primary .md-nav { + display: block; } + .md-tabs--active ~ .md-main .md-nav--primary > .md-nav__list > .md-nav__item { + font-size: 0; + visibility: hidden; } + .md-tabs--active ~ .md-main .md-nav--primary > .md-nav__list > .md-nav__item--nested { + display: none; + font-size: 1.4rem; + overflow: auto; + visibility: visible; } + .md-tabs--active ~ .md-main .md-nav--primary > .md-nav__list > .md-nav__item--nested > .md-nav__link { + display: none; } + .md-tabs--active ~ .md-main .md-nav--primary > .md-nav__list > .md-nav__item--active { + display: block; } + .md-tabs--active ~ .md-main .md-nav[data-md-level="1"] { + max-height: initial; + overflow: visible; } + .md-tabs--active ~ .md-main .md-nav[data-md-level="1"] > .md-nav__list > .md-nav__item { + padding-left: 0; } + .md-tabs--active ~ .md-main .md-nav[data-md-level="1"] .md-nav .md-nav__title { + display: none; } } + +@media only screen and (min-width: 45em) { + .md-footer-nav__link { + width: 50%; } + .md-footer-copyright { + max-width: 75%; + float: left; } + [dir="rtl"] .md-footer-copyright { + float: right; } + .md-footer-social { + padding: 1.2rem 0; + float: right; } + [dir="rtl"] .md-footer-social { + float: left; } } + +@media only screen and (max-width: 29.9375em) { + [data-md-toggle="search"]:checked ~ .md-header .md-search__overlay { + -webkit-transform: scale(45); + transform: scale(45); } } + +@media only screen and (min-width: 30em) and (max-width: 44.9375em) { + [data-md-toggle="search"]:checked ~ .md-header .md-search__overlay { + -webkit-transform: scale(60); + transform: scale(60); } } + +@media only screen and (min-width: 45em) and (max-width: 59.9375em) { + [data-md-toggle="search"]:checked ~ .md-header .md-search__overlay { + -webkit-transform: scale(75); + transform: scale(75); } } + +@media only screen and (min-width: 60em) and (max-width: 76.1875em) { + [data-md-toggle="search"]:checked ~ .md-header .md-search__inner { + width: 46.8rem; } + .md-search__scrollwrap { + width: 46.8rem; } + .md-search-result__teaser { + max-height: 5rem; + -webkit-line-clamp: 3; } } + +/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsImZpbGUiOiJhc3NldHMvc3R5bGVzaGVldHMvYXBwbGljYXRpb24uNDUxZjgwZTUuY3NzIiwic291cmNlUm9vdCI6IiJ9*/ \ No newline at end of file diff --git a/v1/common/docs/adalclient/index.html b/v1/common/docs/adalclient/index.html new file mode 100644 index 000000000..5f414b9a7 --- /dev/null +++ b/v1/common/docs/adalclient/index.html @@ -0,0 +1,1994 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + adalclient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+ +
+ + +
+
+ + + + + +

@pnp/core/adalclient

+

Added in 1.0.4

+

This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with +SharePoint Framework's permissions.

+

Setup and Use inside SharePoint Framework

+

Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes. This method only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined above using the constructor to specify the values for an AAD Application you have setup.

+

Calling the graph api

+

By providing the context in the onInit we can create the adal client from known information.

+
import { graph } from "@pnp/graph";
+import { getRandomString } from "@pnp/core";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+    graph.setup({
+      spfxContext: this.context
+    });
+  });
+}
+
+public render(): void {
+
+  // here we are creating a team with a random name, required Group ReadWrite All permissions
+  const teamName = `ATeam.${getRandomString(4)}`;
+
+  this.domElement.innerHTML = `Hello, I am creating a team named "${teamName}" for you...`;
+
+  graph.teams.create(teamName, "This is a description").then(t => {
+
+    this.domElement.innerHTML += "done!";
+
+  }).catch(e => {
+
+    this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`;
+  });
+}
+
+ + +

Calling the SharePoint API

+

This example shows how to use the ADALClient with the @pnp/sp library to call

+
import { sp } from "@pnp/sp";
+import { AdalClient } from "@pnp/core";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+    sp.setup({
+      spfxContext: this.context,
+      sp: {
+        fetchClientFactory: () => ,
+      },
+    });
+
+  });
+}
+
+public render(): void {
+
+  sp.web.get().then(t => {
+    this.domElement.innerHTML = JSON.stringify(t);
+  }).catch(e => {
+    this.domElement.innerHTML = JSON.stringify(e);
+  });
+}
+
+ + +

Calling the any API

+

You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example.

+
import { AdalClient, FetchOptions } from "@pnp/core";
+import { ODataDefaultParser } from "@pnp/queryable";
+
+// ...
+
+public render(): void {
+
+  // create an ADAL Client
+  const client = AdalClient.fromSPFxContext(this.context);
+
+  // setup the request options
+  const opts: FetchOptions = {
+    method: "GET",
+    headers: {
+      "Accept": "application/json",
+    },
+  };
+
+  // execute the request
+  client.fetch("https://tenant.sharepoint.com/_api/web", opts).then(response => {
+
+    // create a parser to convert the response into JSON.
+    // You can create your own, at this point you have a fetch Response to work with
+    const parser = new ODataDefaultParser();
+
+    parser.parse(response).then(json => {
+      this.domElement.innerHTML = JSON.stringify(json);
+    });
+
+  }).catch(e => {
+    this.domElement.innerHTML = JSON.stringify(e);
+  });
+
+}
+
+ + +

Manually Configure

+

This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD.

+

Setup and Use with Microsoft Graph

+

This sample uses a custom AzureAd app you have created and granted the appropriate permissions.

+
import { AdalClient } from "@pnp/core";
+import { graph } from "@pnp/graph";
+
+// configure the graph client
+// parameters are:
+// client id - the id of the application you created in azure ad
+// tenant - can be id or URL (shown)
+// redirect url - absolute url of a page to which your application and Azure AD app allows replies
+graph.setup({
+    graph: {
+        fetchClientFactory: () => {
+            return new AdalClient(
+                "e3e9048e-ea28-423b-aca9-3ea931cc7972",
+                "{tenant}.onmicrosoft.com",
+                "https://myapp/singlesignon.aspx");
+        },
+    },
+});
+
+try {
+
+    // call the graph API
+    const groups = await graph.groups.get();
+
+    console.log(JSON.stringify(groups, null, 4));
+
+} catch (e) {
+    console.error(e);
+}
+
+ + +

Nodejs Applications

+

We have a dedicated node client in @pnp/nodejs.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/collections/index.html b/v1/common/docs/collections/index.html new file mode 100644 index 000000000..6e0751122 --- /dev/null +++ b/v1/common/docs/collections/index.html @@ -0,0 +1,1786 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + collections - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/core/collections

+

The collections module provides typings and classes related to working with dictionaries.

+

TypedHash

+

Interface used to described an object with string keys corresponding to values of type T

+
export interface TypedHash<T> {
+    [key: string]: T;
+}
+
+ + +

objectToMap

+

Converts a plain object to a Map instance

+
const map = objectToMap({ a: "b", c: "d"});
+
+ + +

mergeMaps

+

Merges two or more maps, overwriting values with the same key. Last value in wins.

+
const m1 = new Map();
+const m2 = new Map();
+const m3 = new Map();
+const m4 = new Map();
+
+const m = mergeMaps(m1, m2, m3, m4);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/custom-httpclientimpl/index.html b/v1/common/docs/custom-httpclientimpl/index.html new file mode 100644 index 000000000..0e6096316 --- /dev/null +++ b/v1/common/docs/custom-httpclientimpl/index.html @@ -0,0 +1,1798 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Custom HttpClientImpl - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Custom HttpClientImpl

+

This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation.

+

It is possible you may need complete control over the sending and receiving of requests.

+

Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation.

+

The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl.

+
export interface HttpClientImpl {
+    fetch(url: string, options: FetchOptions): Promise<Response>;
+}
+
+ + +

There is a single method "fetch" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface.

+
export interface FetchOptions {
+    method?: string;
+    headers?: HeadersInit | { [index: string]: string };
+    body?: BodyInit;
+    mode?: string | RequestMode;
+    credentials?: string | RequestCredentials;
+    cache?: string | RequestCache;
+}
+
+ + +

So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read 👍.

+

Using Your Custom HttpClientImpl

+

Once you have written your implementation using it on your requests is done by setting it in the global library configuration:

+
import { setup } from "@pnp/core";
+import { sp, Web } from "@pnp/sp";
+import { MyAwesomeClient } from "./awesomeclient";
+
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new MyAwesomeClient();
+        }
+    }
+});
+
+let w = new Web("{site url}");
+
+// this request will use your client.
+w.select("Title").get().then(w => {
+    console.log(w);
+});
+
+ + +

Subclassing is Better

+

You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation.

+

A FINAL NOTE

+

Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/index.html b/v1/common/docs/index.html new file mode 100644 index 000000000..7d5ab9f27 --- /dev/null +++ b/v1/common/docs/index.html @@ -0,0 +1,1787 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + common - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/core

+

npm version

+

The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well.

+

Getting Started

+

Install the library and required dependencies

+

npm install @pnp/core --save

+

Import and use functionality, see details on modules below.

+
import { getGUID } from "@pnp/core";
+
+console.log(getGUID());
+
+ + +

Exports

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/core. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/libconfig/index.html b/v1/common/docs/libconfig/index.html new file mode 100644 index 000000000..8c53dc4f3 --- /dev/null +++ b/v1/common/docs/libconfig/index.html @@ -0,0 +1,1930 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + libconfig - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/core/libconfig

+

Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core +configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and +contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications.

+

LibraryConfiguration Interface

+

Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below

+
export interface LibraryConfiguration {
+
+    /**
+     * Allows caching to be global disabled, default: false
+     */
+    globalCacheDisable?: boolean;
+
+    /**
+     * Defines the default store used by the usingCaching method, default: session
+     */
+    defaultCachingStore?: "session" | "local";
+
+    /**
+     * Defines the default timeout in seconds used by the usingCaching method, default 30
+     */
+    defaultCachingTimeoutSeconds?: number;
+
+    /**
+     * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval
+     */
+    enableCacheExpiration?: boolean;
+
+    /**
+     * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100)
+     */
+    cacheExpirationIntervalMilliseconds?: number;
+
+    /**
+     * Used to supply the current context from an SPFx webpart to the library
+     */
+    spfxContext?: any;
+}
+
+ + +

RuntimeConfigImpl

+

The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary +used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method.

+

extend

+

The extend method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any +existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please +see the section below "Using RuntimeConfig within your application". Note there are no methods to remove/clear the global config as it should be considered fairly static +as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application.

+
import { RuntimeConfig } from "@pnp/core";
+
+// add your custom keys to the global configuration
+// note you can use object hashes as values
+RuntimeConfig.extend({
+   "myKey1": "value 1",
+   "myKey2": {
+       "subKey": "sub value 1",
+       "subKey2": "sub value 2",
+   },
+});
+
+// read your custom values
+const v = RuntimeConfig.get("myKey1"); // "value 1"
+
+ + +

Using RuntimeConfig within your Application

+

If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To +do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties

+
import { LibraryConfiguration, RuntimeConfig } from "@pnp/core";
+
+// first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because
+// TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions.
+
+// create the interface of your properties
+// by creating this separately you allows others to compose your parts into their own config
+interface MyConfigurationPart {
+
+    // you can create a grouped definition and access your settings as an object
+    // keys can be optional or required as defined by your interface
+    my?: {
+        prop1?: string;
+        prop2?: string;
+    }
+
+    // and/or define multiple top level properties (beware key collision)
+    // it is good practice to use a unique prefix
+    myProp1: string;
+    myProp2: number;
+}
+
+// now create a combined interface
+interface MyConfiguration extends LibraryConfiguration, MyConfigurationPart { }
+
+
+// now create a wrapper object and expose your properties
+class MyRuntimeConfigImpl {
+
+    // exposing a nested property
+    public get prop1(): TypedHash<string> {
+
+        const myPart = RuntimeConfig.get("my");
+        if (myPart !== null && typeof myPart !== "undefined" && typeof myPart.prop1 !== "undefined") {
+            return myPart.prop1;
+        }
+
+        return {};
+    }
+
+    // exposing a root level property
+    public get myProp1(): string | null {
+
+        let myProp1 = RuntimeConfig.get("myProp1");
+
+        if (myProp1 === null) {
+            myProp1 = "some default value";
+        }
+
+        return myProp1;
+    }
+
+    setup(config: MyConfiguration): void {
+        RuntimeConfig.extend(config);
+    }
+}
+
+// create a single static instance of your impl class
+export let MyRuntimeConfig = new MyRuntimeConfigImpl();
+
+ + +

Now in other files you can use and set your configuration with a typed interface and properties

+
import { MyRuntimeConfig } from "{location of module}";
+
+
+MyRuntimeConfig.setup({
+    my: {
+        prop1: "hello",
+    },
+});
+
+const value = MyRuntimeConfig.prop1; // "hello"
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/netutil/index.html b/v1/common/docs/netutil/index.html new file mode 100644 index 000000000..6ee9e809a --- /dev/null +++ b/v1/common/docs/netutil/index.html @@ -0,0 +1,1860 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + netutil - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/core/netutil

+

This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces +are described below (many have no use outside the library) as well as several classes.

+

Interfaces

+

HttpClientImpl

+

Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method "fetch" take a URL and +options and returning a Promise. Used primarily with the shared request pipeline to define the client used to make the actual request. You can +write your own custom implementation if needed.

+

RequestClient

+

An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The +difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the +underlying HttpClientImpl fetch method.

+

Classes

+

This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl.

+

FetchClient

+

Basic implementation that calls the global (window) fetch method with no additional processing.

+
import { FetchClient } from "@pnp/core";
+
+const client = new FetchClient();
+
+client.fetch("{url}", {});
+
+ + +

BearerTokenFetchClient

+

A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and +the token is treated as a static string.

+
import { BearerTokenFetchClient } from "@pnp/core";
+
+const client = new BearerTokenFetchClient("{authentication token}");
+
+client.fetch("{url}", {});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/storage/index.html b/v1/common/docs/storage/index.html new file mode 100644 index 000000000..66a8ea739 --- /dev/null +++ b/v1/common/docs/storage/index.html @@ -0,0 +1,1850 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + storage - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/core/storage

+

This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with +a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.

+

PnPClientStorage

+

The main export of this module, contains properties representing local and session storage.

+
import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+const myvalue = storage.local.get("mykey");
+
+ + +

PnPClientStorageWrapper

+

Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used +from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.

+
import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+
+// get a value from storage
+const value = storage.local.get("mykey");
+
+// put a value into storage
+storage.local.put("mykey2", "my value");
+
+// put a value into storage with an expiration
+storage.local.put("mykey2", "my value", new Date());
+
+// put a simple object into storage
+// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects
+storage.local.put("mykey3", {
+    key: "value",
+    key2: "value2",
+});
+
+// remove a value from storage
+storage.local.delete("mykey3");
+
+// get an item or add it if it does not exist
+// returns a promise in case you need time to get the value for storage
+// optionally takes a third parameter specifying the expiration
+storage.local.getOrPut("mykey4", () => {
+    return Promise.resolve("value");
+});
+
+// delete expired items
+storage.local.deleteExpired();
+
+ + +

Cache Expiration

+

The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.

+
import { PnPClientStorage } from "@pnp/core";
+
+const storage = new PnPClientStorage();
+
+// session storage
+storage.session.deleteExpired();
+
+// local storage
+storage.local.deleteExpired();
+
+// this returns a promise, so you can perform some activity after the expired items are removed:
+storage.local.deleteExpired().then(_ => {
+    // init my application
+});
+
+ + +

The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout.

+
import { setup } from "@pnp/core";
+
+setup({
+    enableCacheExpiration: true,
+    cacheExpirationIntervalMilliseconds: 1000, // optional
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/common/docs/util/index.html b/v1/common/docs/util/index.html new file mode 100644 index 000000000..429bb8239 --- /dev/null +++ b/v1/common/docs/util/index.html @@ -0,0 +1,2058 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + util - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/core/util

+

This module contains utility methods that you can import individually from the common library.

+
import {
+    getRandomString,
+} from "@pnp/core";
+
+// use from individual;y imported method
+console.log(getRandomString(10));
+
+ + +

getCtxCallback

+

Gets a callback function which will maintain context across async calls.

+
import { getCtxCallback } from "@pnp/core";
+
+const contextThis = {
+    myProp: 6,
+};
+
+function theFunction() {
+    // "this" within this function will be the context object supplied
+    // in this case the variable contextThis, so myProp will exist
+    return this.myProp;
+}
+
+const callback = getCtxCallback(contextThis, theFunction);
+
+callback(); // returns 6
+
+// You can also supply additional parameters if needed
+
+function theFunction2(g: number) {
+    // "this" within this function will be the context object supplied
+    // in this case the variable contextThis, so myProp will exist
+    return this.myProp + g;
+}
+
+const callback2 = getCtxCallback(contextThis, theFunction, 4);
+
+callback2(); // returns 10 (6 + 4)
+
+ + +

dateAdd

+

Manipulates a date, please see the Stackoverflow discussion from where this method was taken.

+

combine

+

Combines any number of paths, normalizing the slashes as required

+
import { combine } from "@pnp/core";
+
+// "https://microsoft.com/something/more"
+const paths = combine("https://microsoft.com", "something", "more");
+
+// "also/works/with/relative"
+const paths2 = combine("/also/", "/works", "with/", "/relative\\");
+
+ + +

getRandomString

+

Gets a random string consisting of the number of characters requested.

+
import { getRandomString } from "@pnp/core";
+
+const randomString = getRandomString(10);
+
+ + +

getGUID

+

Creates a random guid, please see the Stackoverflow discussion from where this method was taken.

+

isFunc

+

Determines if a supplied variable represents a function.

+

objectDefinedNotNull

+

Determines if an object is defined and not null.

+

isArray

+

Determines if a supplied variable represents an array.

+

extend

+

Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing +properties.

+
import { extend } from "@pnp/core";
+
+let obj1 = {
+    prop: 1,
+    prop2: 2,
+};
+
+const obj2 = {
+    prop: 4,
+    prop3: 9,
+};
+
+const example1 = extend(obj1, obj2);
+// example1 = { prop: 4, prop2: 2, prop3: 9 }
+
+const example2 = extend(obj1, obj2, true);
+// example2 = { prop: 1, prop2: 2, prop3: 9 }
+
+ + +

isUrlAbsolute

+

Determines if a supplied url is absolute and returns true; otherwise returns false.

+

stringIsNullOrEmpty

+

Determines if a supplied string is null or empty

+

Removed

+

Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods +below for use in your projects should you require.

+
/**
+ * Loads a stylesheet into the current page
+ *
+ * @param path The url to the stylesheet
+ * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues
+ */
+public static loadStylesheet(path: string, avoidCache: boolean): void {
+    if (avoidCache) {
+        path += "?" + encodeURIComponent((new Date()).getTime().toString());
+    }
+    const head = document.getElementsByTagName("head");
+    if (head.length > 0) {
+        const e = document.createElement("link");
+        head[0].appendChild(e);
+        e.setAttribute("type", "text/css");
+        e.setAttribute("rel", "stylesheet");
+        e.setAttribute("href", path);
+    }
+}
+
+/**
+ * Tests if a url param exists
+ *
+ * @param name The name of the url parameter to check
+ */
+public static urlParamExists(name: string): boolean {
+    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
+    const regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
+    return regex.test(location.search);
+}
+
+/**
+ * Gets a url param value by name
+ *
+ * @param name The name of the parameter for which we want the value
+ */
+public static getUrlParamByName(name: string): string {
+    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
+    const regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
+    const results = regex.exec(location.search);
+    return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
+}
+
+/**
+ * Gets a url param by name and attempts to parse a bool value
+ *
+ * @param name The name of the parameter for which we want the boolean value
+ */
+public static getUrlParamBoolByName(name: string): boolean {
+    const p = this.getUrlParamByName(name);
+    const isFalse = (p === "" || /false|0/i.test(p));
+    return !isFalse;
+}
+
+/**
+ * Inserts the string s into the string target as the index specified by index
+ *
+ * @param target The string into which we will insert s
+ * @param index The location in target to insert s (zero based)
+ * @param s The string to insert into target at position index
+ */
+public static stringInsert(target: string, index: number, s: string): string {
+    if (index > 0) {
+        return target.substring(0, index) + s + target.substring(index, target.length);
+    }
+    return s + target;
+}
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/config-store/docs/configuration/index.html b/v1/config-store/docs/configuration/index.html new file mode 100644 index 000000000..8f8988e2c --- /dev/null +++ b/v1/config-store/docs/configuration/index.html @@ -0,0 +1,1730 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + configuration - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/config-store/configuration

+

The main class exported from the config-store package is Settings. This is the class through which you will load and access your +settings via providers.

+
import { Web } from "@pnp/sp";
+import { Settings, SPListConfigurationProvider } from "@pnp/config-store";
+
+// create an instance of the settings class, could be static and shared across your application
+// or built as needed.
+const settings = new Settings();
+
+// you can add/update a single value using add
+settings.add("mykey", "myvalue");
+
+// you can also add/update a JSON value which will be stringified for you as a shorthand
+settings.addJSON("mykey2", {
+    field: 1,
+    field2: 2,
+    field3: 3,
+});
+
+// and you can apply a plain object of keys/values that will be written as single values
+// this results in each enumerable property of the supplied object being added to the settings collection
+settings.apply({
+    field: 1,
+    field2: 2,
+    field3: 3,
+});
+
+// and finally you can load values from a configuration provider
+const w = new Web("https://mytenant.sharepoint.com/sites/dev");
+const provider = new SPListConfigurationProvider(w, "myconfiglistname");
+
+// this will load values from the supplied list
+// by default the key will be from the Title field and the value from a column named Value
+await settings.load(provider);
+
+// once we have loaded values we can then read them
+const value = settings.get("mykey");
+
+// or read JSON that will be parsed for you from the store
+const value2 = settings.getJSON("mykey2");
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/config-store/docs/index.html b/v1/config-store/docs/index.html new file mode 100644 index 000000000..0bd4fbd66 --- /dev/null +++ b/v1/config-store/docs/index.html @@ -0,0 +1,1761 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + config-store - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/config-store

+

npm version

+

This module providers a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed.

+

Getting Started

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/config-store --save

+

See the topics below for usage:

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/config-store. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/config-store/docs/providers/index.html b/v1/config-store/docs/providers/index.html new file mode 100644 index 000000000..9f3e2853d --- /dev/null +++ b/v1/config-store/docs/providers/index.html @@ -0,0 +1,1782 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + providers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/config-store/providers

+

Currently there is a single provider included in the library, but contributions of additional providers are welcome.

+

SPListConfigurationProvider

+

This provider is based on a SharePoint list and read all of the rows and makes them available as a TypedHash. By default the column names used are Title for key and "Value" for value, but you can update these as needed. Additionally the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence.

+
import { Web } from "@pnp/sp";
+import { Settings, SPListConfigurationProvider } from "@pnp/config-store";
+
+// create a new provider instance
+const w = new Web("https://mytenant.sharepoint.com/sites/dev");
+const provider = new SPListConfigurationProvider(w, "myconfiglistname");
+
+const settings = new Settings();
+
+// load our values from the list
+await settings.load(provider);
+
+ + +

CachingConfigurationProvider

+

Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a +provider and caches the configuration in local or session storage.

+
import { Web } from "@pnp/sp";
+import { Settings, SPListConfigurationProvider } from "@pnp/config-store";
+
+// create a new provider instance
+const w = new Web("https://mytenant.sharepoint.com/sites/dev");
+const provider = new SPListConfigurationProvider(w, "myconfiglistname");
+
+// get an instance of the provider wrapped
+// you can optionally provide a key that will be used in the cache to the asCaching method
+const wrappedProvider = provider.asCaching();
+
+// use that wrapped provider to populate the settings
+await settings.load(wrappedProvider);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/SPFx-On-Premesis-2016/index.html b/v1/documentation/SPFx-On-Premesis-2016/index.html new file mode 100644 index 000000000..cc149679d --- /dev/null +++ b/v1/documentation/SPFx-On-Premesis-2016/index.html @@ -0,0 +1,1720 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SPFx On-Premises 2016 - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Workaround for SPFx TypeScript Version

+

Note this article applies to version 1.4.1 SharePoint Framework projects targetting on-premesis only.

+

When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premesis it installs TypeScript version 2.2.2. Unfortunately this library relies on 2.4.2 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article.

+
    +
  1. Open package-lock.json
  2. +
  3. Search for "typescript": "2.2.2"
  4. +
  5. Replace "2.2.2" with "2.4.2"
  6. +
  7. Search for the next "typescript" occurance and replace the block with:
  8. +
+
"typescript": {
+  "version": "2.4.2",
+  "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz",
+  "integrity": "sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=",
+  "dev": true
+}
+
+ + +

Replacement blocks highlighted

+
    +
  1. Remove node_modules folder rm -rf node_modules/
  2. +
  3. Run npm install
  4. +
+

This can be checked with:

+
npm list typescript
+
+ + +
+-- @microsoft/sp-build-web@1.1.0
+| `-- @microsoft/gulp-core-build-typescript@3.1.1
+|   +-- @microsoft/api-extractor@2.3.8
+|   | `-- typescript@2.4.2
+|   `-- typescript@2.4.2
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/beta-versions/index.html b/v1/documentation/beta-versions/index.html new file mode 100644 index 000000000..869e59475 --- /dev/null +++ b/v1/documentation/beta-versions/index.html @@ -0,0 +1,1742 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Install Beta Versions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Beta Versions

+

To help folks try out new features early and provide feedback prior to releases we publish beta versions of the packages. Released as a set with matching version numbers, just like when we do a normal release. Generally every Friday a new set of beta libraries will be released. While not ready for production use we encourage you to try out these pre-release packages and provide us feedback.

+

Installing

+

To install the beta packages in your project you use the @beta version number on the packages. This applies to all packages, not just the ones +shown in the example below.

+
npm install @pnp/logging@beta @pnp/core@beta @pnp/queryable@beta @pnp/sp@beta --save
+
+ + +

Please remember that it is possible something may not work in a beta version, so be aware and if you find something please report an +issue.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/css/extra.css b/v1/documentation/css/extra.css new file mode 100644 index 000000000..6391b1ff7 --- /dev/null +++ b/v1/documentation/css/extra.css @@ -0,0 +1,33 @@ +.md-logo { + height: 32px; + width: 150px; + padding: 0 0.25 0.5 !important; +} + +.md-header{ + height: 75px; +} + +.md-container{ + padding-top: 70px; +} + +.md-sidebar[data-md-state="lock"]{ + padding-top: 75px; +} + +.md-logo img { + width: 100% !important; + height: auto !important; + margin-top: -0.25em; +} + +.md-footer { + margin-top: 5em; +} + +@media only screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__title--site .md-nav__button { + width: 150px; + } +} \ No newline at end of file diff --git a/v1/documentation/debugging/index.html b/v1/documentation/debugging/index.html new file mode 100644 index 000000000..5d43b1e41 --- /dev/null +++ b/v1/documentation/debugging/index.html @@ -0,0 +1,2098 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Debugging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

Debugging

+

Debugging Library Features in Code using Node

+

The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses the launch.json file to build and run the library using ./debug/launch/main.ts as the program entry. You can add any number of files to this directory and they will be ignored by git, however the debug.ts file is not, so please ensure you don't commit any login information.

+

Setup settings.js

+

If you have not already you need to create a settings.js files by copying settings.example.js and renaming it to settings.js. Then update the clientId, clientSecret, and siteUrl fields in the testing section. (See below for guidance on registering a client id and secret)

+

Test your setup

+

If you hit F5 now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.

+

Create a debug module

+

Using ./debug/launch/example.ts as a reference create a debugging file in the debug folder, let's call it mydebug.ts and add this content:

+
// note we can use the actual package names for our imports
+import { sp, ListEnsureResult } from "@pnp/sp";
+import { Logger, LogLevel, ConsoleListener } from "@pnp/logging";
+
+declare var process: { exit(code?: number): void };
+
+export function MyDebug() {
+
+    // run some debugging
+    sp.web.lists.ensure("MyFirstList").then((list: ListEnsureResult) => {
+
+        Logger.log({
+            data: list.created,
+            message: "Was list created?",
+            level: LogLevel.Verbose
+        });
+
+        if (list.created) {
+
+            Logger.log({
+                data: list.data,
+                message: "Raw data from list creation.",
+                level: LogLevel.Verbose
+            });
+
+        } else {
+
+            Logger.log({
+                data: null,
+                message: "List already existed!",
+                level: LogLevel.Verbose
+            });
+        }
+
+        process.exit(0);
+    }).catch(e => {
+
+        Logger.error(e);
+        process.exit(1);
+    });
+}
+
+ + +

Update main.ts to launch your module

+

First comment out the import for the default example and then add the import and function call for yours, the updated main.ts should look like this:

+
// ...
+
+// comment out the example
+// import { Example } from "./example";
+// Example();
+
+import { MyDebug } from "./mydebug"
+MyDebug();
+
+// ...
+
+ + +

Debug!

+

Place a break point within the promise resolution in your debug file and hit F5. Your module should be run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.

+

Next Steps

+

Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally.

+

In Browser Debugging

+

You can also serve files locally to debug in a browser through two methods. The first will serve code using ./debug/serve/main.ts as the entry. Meaning you can easily +write code and test it in the browser. The second method allows you to serve a single package (bundled with all dependencies) for in browser testing. Both methods serve +the file from https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing.

+

Start the local serve

+

This will serve a package with ./debug/serve/main.ts as the entry.

+

gulp serve

+

Add reference to library

+

Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.

+
<script src="https://localhost:8080/assets/pnp.js"></script>
+<div id="pnptestdiv"></div>
+
+ + +

You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but note that any changes +included as part of a PR to this file will not be allowed.

+

Serve a specific package

+

For example if you wanted to serve the @pnp/sp package for testing you would use:

+

gulp serve --p sp

+

This will serve a bundle of the sp functionality along with all dependencies and place a global variable named "pnp.{packagename}", in this case pnp.sp. This will be +true for each package, if you served just the graph package the global would be pnp.graph. This mirrors how the umd modules are built in the distributed npm packages +to allow testing with matching packages.

+

Next Steps

+

You can make changes to the library and immediately see them reflected in the browser. All files are watched regardless of which serve method you choose.

+

Register an Add-in

+

Before you can begin debugging you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly.

+
    +
  1. Navigation to {site url}/_layouts/appregnew.aspx
  2. +
  3. Click "Generate" for both the Client Id and Secret values
  4. +
  5. Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions
  6. +
  7. Provide a fake value for app domain and redirect uri, you can use the values shown in the examples
  8. +
  9. Click "Create"
  10. +
  11. Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.
  12. +
+

Grant Your Add-In Permissions

+

Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site.

+
    +
  1. Navigate to {admin site url}/_layouts/appinv.aspx
  2. +
  3. Paste your client id from the above section into the Add Id box and click "Lookup"
  4. +
  5. You should see the information populated into the form from the last section, if not ensure you have the correct id value
  6. +
  7. Paste the below XML into the permissions request xml box and hit "Create"
  8. +
  9. You should get a confirmation message.
  10. +
+
  <AppPermissionRequests AllowAppOnlyPolicy="true">
+    <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" />
+    <AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" />
+    <AppPermissionRequest Scope="http://sharepoint/search" Right="QueryAsUserIgnoreAppPrincipal" />
+  </AppPermissionRequests>
+
+ + +

Note these are very broad permissions to ensure you can test any feature of the library, for production you should tailor the permissions to only those required

+

Configure the project settings file

+
    +
  1. If you have not already, make a copy of settings.example.js and name it settings.js
  2. +
  3. Edit this file to set the values on the testing.sp object to
      +
    • id: "The client id you created"
    • +
    • secret: "The client secret you created"
    • +
    • url: "{site url}"
    • +
    +
  4. +
  5. You can disable web tests at any time by setting enableWebTests to false in settings.js, this can be helpful as they take a few minutes to run
  6. +
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/deployment/index.html b/v1/documentation/deployment/index.html new file mode 100644 index 000000000..971317f28 --- /dev/null +++ b/v1/documentation/deployment/index.html @@ -0,0 +1,1972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Deployment - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Deployment

+

There are two recommended ways to consume the library in a production deployment: bundle the code into your solution (such as with webpack), or reference the code from a CDN. These methods are outlined here but this is not meant to be an exhaustive guide on all the ways to package and deploy solutions.

+

Bundle

+

If you have installed the library via NPM into your application solution bundlers such as webpack can bundle the PnPjs libraries along with your solution. This can make deployment easier, but will increase the size of your application by the size of the included libraries. The PnPjs libraries are setup to support tree shaking which can help with the bundle size.

+

CDN

+

If you have public internet access you can reference the library from cdnjs or unpkg which maintains copies of all versions. This is ideal as you do not need to host the file yourself, and it is easy to update to a newer release by updating the URL in your solution. Below lists all of the library locations within cdnjs, you will need to ensure you have the full url to the file you need, such as: "https://cdnjs.cloudflare.com/ajax/libs/pnp-common/1.1.1/common.es5.umd.min.js". To use the libraries with a script tag in a page it is recommended to use the *.es5.umd.min.js versions. This will add a global pnp value with each library added as pnp.{lib name} such as pnp.sp, pnp.common, etc.

+ +

CDN and SPFx

+

If you are developing in SPFx and install and import the PnPjs libraries the default behavior will be to bundle the library into your solution. You have a couple of choices on how best to work with CDNs and SPFx. Because SPFx doesn't currently respect peer dependencies it is easier to reference the pnpjs rollup package for your project. In this case you would install the package, reference it in your code, and update your config.js file externals as follows:

+

Install

+

npm install @pnp/pnpjs --save

+

In Code

+
import { sp } from "@pnp/pnpjs";
+
+sp.web.lists.getByTitle("BigList").get().then(r => {
+
+    this.domElement.innerHTML += r.Title;
+});
+
+ + +

config.json

+
  "externals": {
+    "@pnp/pnpjs": {
+      "path": "https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.1.4/pnpjs.es5.umd.bundle.min.js",
+      "globalName": "pnp"
+    }
+  },
+
+ + +
+

You can still work with the individual packages from a cdn, but you have a bit more work to do. First install the modules you need, update the config with the JSON externals below, and add some blind require statements into your code. These are needed because peer dependencies are not processed by SPFx so you have to "trigger" the SPFx manifest creator to include those packages.

+
+

Note this approach requires using version 1.1.5 (specifically beta 1.1.5-2) or later of the libraries as we had make a few updates to how things are packaged to make this a little easier.

+
+

Install

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save

+

In Code

+
// blind require statements
+require("tslib");
+require("@pnp/logging");
+require("@pnp/core");
+require("@pnp/queryable");
+import { sp } from "@pnp/sp";
+
+sp.web.lists.getByTitle("BigList").get().then(r => {
+
+    this.domElement.innerHTML += r.Title;
+});
+
+ + +

config.json

+
"externals": {
+  "@pnp/sp": {
+    "path": "https://unpkg.com/@pnp/sp@1.1.5-2/dist/sp.es5.umd.min.js",
+    "globalName": "pnp.sp",
+    "globalDependencies": [
+      "@pnp/logging",
+      "@pnp/core",
+      "@pnp/queryable",
+      "tslib"
+    ]
+  },
+  "@pnp/queryable": {
+    "path": "https://unpkg.com/@pnp/queryable@1.1.5-2/dist/odata.es5.umd.min.js",
+    "globalName": "pnp.odata",
+    "globalDependencies": [
+      "@pnp/core",
+      "@pnp/logging",
+      "tslib"
+    ]
+  },
+  "@pnp/core": {
+    "path": "https://unpkg.com/@pnp/core@1.1.5-2/dist/common.es5.umd.bundle.min.js",
+    "globalName": "pnp.common"     
+  },
+  "@pnp/logging": {
+    "path": "https://unpkg.com/@pnp/logging@1.1.5-2/dist/logging.es5.umd.min.js",
+    "globalName": "pnp.logging",
+    "globalDependencies": [
+      "tslib"
+    ]
+  },
+  "tslib": {
+    "path": "https://cdnjs.cloudflare.com/ajax/libs/tslib/1.9.3/tslib.min.js",
+    "globalName": "tslib"
+  }
+}
+
+ + +

Don't forget to update the version number in the url to match the version you want to use. This will stop the library from being bundled directly into the solution and instead use the copy from the CDN. When a new version of the PnPjs libraries are released and you are ready to update just update this url in your SPFX project's config.js file.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/documentation/index.html b/v1/documentation/documentation/index.html new file mode 100644 index 000000000..2376a396c --- /dev/null +++ b/v1/documentation/documentation/index.html @@ -0,0 +1,1760 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Building Docs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Building the Documentation

+

Building the documentation locally can help you visualize change you are making to the docs. What you see locally should be what you see online.

+

Building

+

Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.

+

When executing the pip module on Windows you can prefix it with python -m. +For example:

+
python -m pip install mkdocs-material
+
+ + +
    +
  • Install MkDocs
      +
    • pip install mkdocs
    • +
    +
  • +
  • Install the Material theme
      +
    • pip install mkdocs-material
    • +
    +
  • +
  • install the mkdocs-markdownextradata-plugin - this is used for the version variable
      +
    • pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7)
    • +
    +
  • +
  • Serve it up
      +
    • mkdocs serve
    • +
    • Open a browser to http://127.0.0.1:8000/
    • +
    +
  • +
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/getting-started-dev/index.html b/v1/documentation/getting-started-dev/index.html new file mode 100644 index 000000000..07a9dfaed --- /dev/null +++ b/v1/documentation/getting-started-dev/index.html @@ -0,0 +1,1799 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Getting Started Contributing - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Contribution Guide

+

Thank you for your interest in contributing to our work. This guide should help you get started, please let us know if you have any questions.

+

Contributor Guidance

+
    +
  • Target your pull requests to the dev branch
  • +
  • Add/Update any relevant docs articles in the relevant package's docs folder related to your changes
  • +
  • Include a test for any new functionality and ensure all existing tests are passing by running gulp test
  • +
  • Ensure tslint checks pass by typing gulp lint
  • +
  • Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work
  • +
  • If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :)
  • +
+

Setup your development environment

+

These steps will help you get your environment setup for contributing to the core library.

+
    +
  1. +

    Install Visual Studio Code - this is the development environment we will use. It is similar to a light-weight Visual Studio designed for each editing of client file types such as .ts and .js. (Note that if you prefer you can use Visual Studio).

    +
  2. +
  3. +

    Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).

    +
  4. +
  5. +

    On Windows: Install Python v2.7.10 - this is used by some of the plug-ins and build tools inside Node JS - (Python v3.x.x is not supported by those modules). If Visual Studio is not installed on the client in addition to this C++ runtime is required. Please see node-gyp Readme

    +
  6. +
  7. +

    Install a console emulator of your choice, for Windows Cmder is popular. If installing Cmder choosing the full option will allow you to use git for windows. Whatever option you choose we will refer in the rest of the guide to "console" as the thing you installed in this step.

    +
  8. +
  9. +

    Install the tslint extension in VS Code:

    +
      +
    1. Press Shift + Ctrl + "p" to open the command panel
    2. +
    3. Begin typing "install extension" and select the command when it appears in view
    4. +
    5. Begin typing "tslint" and select the package when it appears in view
    6. +
    7. Restart Code after installation
    8. +
    +
  10. +
  11. +

    Install the gulp command line globally by typing the following code in your console npm install -g gulp-cli

    +
  12. +
  13. +

    Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.

    +
  14. +
  15. +

    Once you have the code locally, navigate to the root of the project in your console. Type the following command:

    +
  16. +
  17. +

    npm install - installs all of the npm package dependencies (may take awhile the first time)

    +
  18. +
  19. +

    Copy settings.example.js in the root of your project to settings.js. Edit settings.js to reflect your personal environment (usename, password, siteUrl, etc.).

    +
  20. +
  21. +

    Then you can follow the guidance in the debugging article to get started testing right away!

    +
  22. +
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/getting-started/index.html b/v1/documentation/getting-started/index.html new file mode 100644 index 000000000..6bb4f0766 --- /dev/null +++ b/v1/documentation/getting-started/index.html @@ -0,0 +1,2280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Getting Started - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

Getting Started

+

These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install +the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install +more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.

+

If you need to support older browsers please review the article on polyfills for required functionality.

+

Install

+

First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any +environment or project.

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/graph --save

+

Next we can import and use the functionality within our application. The below is a very simple example, please see the individual package documentation +for more details.

+
import { getRandomString } from "@pnp/core";
+
+(function() {
+
+  // get and log a random string
+  console.log(getRandomString(20));
+
+})()
+
+ + +

Getting Started with SharePoint Framework

+

The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 on-premises please read this note on a workaround for the included TypeScript version. If you are targetting SharePoint online you do not need to take any additional steps.

+

Establish Context

+

Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request +urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the spfx context to the library. Either through the setup method +imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if +you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports.

+

The setup is always done in the onInit method to ensure it runs before your other lifecycle code. You can also set any other settings at this time.

+

Using @pnp/core setup

+
import { setup as pnpSetup } from "@pnp/core";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+
+    pnpSetup({
+      spfxContext: this.context
+    });
+  });
+}
+
+// ...
+
+ + +

Using @pnp/sp setup

+
import { sp } from "@pnp/sp";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+
+    sp.setup({
+      spfxContext: this.context
+    });
+  });
+}
+
+// ...
+
+ + +

Using @pnp/graph setup

+
import { graph } from "@pnp/graph";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+
+    graph.setup({
+      spfxContext: this.context
+    });
+  });
+}
+
+// ...
+
+ + +

Establish Context with an SPFx Service

+

Because you do not have access to the full context object within a service you need to setup things slightly differently. This works for the sp library, but not the graph library as we don't have access to the AAD token provider from the full context.

+
import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
+import { PageContext } from "@microsoft/sp-page-context";
+import { AadTokenProviderFactory } from "@microsoft/sp-http";
+import { sp } from "@pnp/sp";
+
+export interface ISampleService {
+  getLists(): Promise<any[]>;
+}
+
+export class SampleService {
+
+  public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService);
+
+  constructor(serviceScope: ServiceScope) {
+
+    serviceScope.whenFinished(() => {
+
+      const pageContext = serviceScope.consume(PageContext.serviceKey);
+      const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);
+
+      // we need to "spoof" the context object with the parts we need for PnPjs
+      sp.setup({
+        spfxContext: {
+          aadTokenProviderFactory: tokenProviderFactory,
+          pageContext: pageContext,
+        }
+      });
+
+      // This approach also works if you do not require AAD tokens
+      // you don't need to do both
+      // sp.setup({
+      //   sp : {
+      //     baseUrl : pageContext.web.absoluteUrl
+      //   }
+      // });
+    });
+  }
+  public getLists(): Promise<any[]> {
+    return sp.web.lists.get();
+  }
+}
+
+ + +

Connect to SharePoint from Node

+

Because peer dependencies are not installed automatically you will need to list out each package to install. Don't worry if you forget one you will get a message +on the command line that a peer dependency is missing. Let's for example look at installing the required libraries to connect to SharePoint from nodejs. You can see +./debug/launch/sp.ts for a live example.

+
npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs
+
+ + +

This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. +Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:

+
import { sp } from "@pnp/sp";
+import { SPFetchClient } from "@pnp/nodejs";
+
+ + +

Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint.

+
// configure your node options (only once in your application)
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPFetchClient("{site url}", "{client id}", "{client secret}");
+        },
+    },
+});
+
+// make a call to SharePoint and log it in the console
+sp.web.select("Title", "Description").get().then(w => {
+    console.log(JSON.stringify(w, null, 4));
+});
+
+ + +

Connect to Microsoft Graph From Node

+

Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see +./debug/launch/graph.ts for a live example.

+
npm i @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs
+
+ + +

Now we need to import what we'll need to call graph

+
import { graph } from "@pnp/graph";
+import { AdalFetchClient } from "@pnp/nodejs";
+
+ + +

Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions.

+
graph.setup({
+    graph: {
+        fetchClientFactory: () => {
+            return new AdalFetchClient("{mytenant}.onmicrosoft.com", "{application id}", "{application secret}");
+        },
+    },
+});
+
+// make a call to Graph and get all the groups
+graph.v1.groups.get().then(g => {
+    console.log(JSON.stringify(g, null, 4));
+});
+
+ + +

Getting Started outside SharePoint Framework

+

In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options.

+

Set baseUrl through setup:

+

Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when +working against unpatched versions of SharePoint 2013 as discussed here. +This is optional for 2016 or SharePoint Online.

+
import { sp } from "@pnp/sp";
+
+sp.setup({
+  sp: {
+    headers: {
+      Accept: "application/json;odata=verbose",
+    },
+    baseUrl: "{Absolute SharePoint Web URL}"
+  },
+});
+
+const w = await sp.web.get();
+
+ + +

Create Web instances directly

+

Using this method you create the web directly with the url you want to use as the base.

+
import { Web } from "@pnp/sp";
+
+const web = new Web("{Absolute SharePoint Web URL}");
+const w = await web.get();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/gulp-commands/index.html b/v1/documentation/gulp-commands/index.html new file mode 100644 index 000000000..623389176 --- /dev/null +++ b/v1/documentation/gulp-commands/index.html @@ -0,0 +1,2100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gulp Commands - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Gulp Commands

+

This library uses Gulp to orchestrate various tasks. The tasks described below are available for your use. Please review the +getting started for development to ensure you've setup your environment correctly. The source for the gulp commands can be found in +the tools\gulptasks folder at the root of the project.

+

Basics

+

All gulp commands are run on the command line in the fashion shown below.

+
gulp <command> [optional pararms]
+
+ + +

build

+

The build command transpiles the solution from TypeScript into JavaScript using our custom build system. It is controlled by the pnp-build.js file at +the project root.

+

Build all of the packages

+
gulp build
+
+ + +

Building individual packages

+

Note when building a single package none of the dependencies are currently built, so you need to specify in order those packages to build which are dependencies.

+
# fails
+gulp build --p sp
+
+# works as all the dependencies are built in order
+gulp build --p logging,common,odata,sp
+
+ + +

You can also build the packages and then not clean using the nc flag. So for example if you are working on the sp package you can build all the packages once, then +use the "nc" flag to leave those that aren't changing.

+
# run once
+gulp build --p logging,common,odata,sp
+
+# run on subsequent builds
+gulp build --p sp --nc
+
+ + +

clean

+

The clean command removes all of the generated folders from the project and is generally used automatically before other commands to ensure there is a clean workspace.

+
gulp clean
+
+ + +

To clean the build folder. This build folder is no longer included in automatic cleaning after the move to use the TypeScript project references feature that compares previous output and doesn't rebuild unchanged files. This command will erase the entire build folder ensuring you can conduct a clean build/test/etc.

+
gulp clean-build
+
+ + +

lint

+

Runs the project linting based on the tslint.json rules defined at the project root. This should be done before any PR submissions as linting failures will block merging.

+
gulp lint
+
+ + +

package

+

Used to create the packages in the ./dist folder as they would exist for a release.

+
gulp package
+
+ + +

Packaging individual packages

+

You can also package individual packages, but as with build you must also package any dependencies at the same time.

+
gulp package --p logging,common,odata,sp
+
+ + +

publish

+

This command is only for use by package authors to publish a version to npm and is not for developer use.

+

serve

+

The serve command allows you to serve either code from the ./debug/serve folder OR an individual package for testing in the browser. The file will always be served as +https://localhost:8080/assets/pnp.js so can create a static page in your tenant for easy testing of a variety of scenarios. NOTE that in most browsers this file will +be flagged as unsafe so you will need to trust it for it to execute on the page.

+

debug serve

+

When running the command with no parameters you will generate a package with the entry being based on the tsconfig.json file in ./debug/serve. By default this will +use serve.ts. This allows you to write any code you want to test to easily run it in the browser with all changes being watched and triggering a rebuild.

+
gulp serve
+
+ + +

package serve

+

If instead you want to test how a particular package will work in the browser you can serve just that package. In this case you do not need to specify the dependencies +and specifying multiple packages will throw an error. Packages will be injected into the global namespace on a variable named pnp.

+
gulp serve --p sp
+
+ + +

test

+

Runs the tests specified in each package's tests folder

+
gulp test
+
+ + +

Verbose

+

The test command will switch to the "spec" mocha reporter if you supply the verbose flag. Doing so will list out each test's description and sucess instead of the "dot" used by default. This flag works with all other test options.

+
gulp test --verbose
+
+ + +

Test individual packages

+

You can test individual packages as needed, and there is no need to include dependencies in this case

+
# test the logging and sp packages
+gulp test --p logging,sp
+
+ + +

If you are working on a specific set of tests for a single module you can also use the single or s parameter to select just +a single module of tests. You specify the filename without the ".test.ts" suffix. It must be within the specified package and +this option can only be used with a single package for --p

+
# will test only the client-side pages module within the sp package
+gulp test --p sp --s clientsidepages
+
+ + +

If you want you can test within the same site and avoid creating a new one, though for some tests this might cause conflicts. +This flag can be helpful if you are rapidly testing things with no conflict as you can avoid creating a site each time. Works +with both of the above options --p and --s as well as individually. The url must be absolute.

+
#testing using the specified site.
+gulp test --site https://{tenant}.sharepoint.com/sites/testing
+
+# with other options
+gulp test --p logging,sp --site https://{tenant}.sharepoint.com/sites/testing
+
+gulp test --p sp --s clientsidepages --site https://{tenant}.sharepoint.com/sites/testing
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/img/Logo.png b/v1/documentation/img/Logo.png new file mode 100644 index 000000000..73dc570b3 Binary files /dev/null and b/v1/documentation/img/Logo.png differ diff --git a/v1/documentation/img/PnPJS_FluentAPI.gif b/v1/documentation/img/PnPJS_FluentAPI.gif new file mode 100644 index 000000000..e65e84fba Binary files /dev/null and b/v1/documentation/img/PnPJS_FluentAPI.gif differ diff --git a/v1/documentation/img/SPFx-On-Premesis-2016-1.png b/v1/documentation/img/SPFx-On-Premesis-2016-1.png new file mode 100644 index 000000000..9ea42e8dd Binary files /dev/null and b/v1/documentation/img/SPFx-On-Premesis-2016-1.png differ diff --git a/v1/documentation/img/office365-header-icon.png b/v1/documentation/img/office365-header-icon.png new file mode 100644 index 000000000..529191ea6 Binary files /dev/null and b/v1/documentation/img/office365-header-icon.png differ diff --git a/v1/documentation/img/pnpjs-common-uml.svg b/v1/documentation/img/pnpjs-common-uml.svg new file mode 100644 index 000000000..4eb24604e --- /dev/null +++ b/v1/documentation/img/pnpjs-common-uml.svg @@ -0,0 +1,220 @@ + + + + +Gyuml_util + +Util + +getCtxCallback +dateAdd +combinePaths +getRandomString +getGUID +isFunc +objectDefinedNotNull +isArray +extend +isUrlAbsolute +stringIsNullOrEmpty +getAttrValueFromString +sanitizeGuid + + +yuml_pnpclientstore + +PnPClientStore + +enabled + +get() +put() +delete() +getOrPut() +deleteExpired() +yuml_pnpclientstoragewrapper + +PnPClientStorageWrapper + +store +defaultTimeoutMinutes +enabled + +get() +put() +delete() +getOrPut() +deleteExpired() +test() +createPersistable() +cacheExpirationHandler() +yuml_pnpclientstore->yuml_pnpclientstoragewrapper + + +yuml_pnpclientstorage + +PnPClientStorage + +_local +_session +local +session + + +yuml_ispfxcontext + +ISPFXContext + +graphHttpClient +pageContext + + +yuml_ispfxgraphhttpclient + +ISPFXGraphHttpClient + + + +fetch() +yuml_fetchclient + +FetchClient + + + +fetch() +yuml_bearertokenfetchclient + +BearerTokenFetchClient + +_token +token + +fetch() +yuml_fetchclient->yuml_bearertokenfetchclient + + +yuml_adalclient + +AdalClient + +clientId +tenant +redirectUri +_authContext +_displayCallback +_loginPromise + +fromSPFxContext() +fetch() +getToken() +ensureAuthContext() +login() +getResource() +yuml_bearertokenfetchclient->yuml_adalclient + + +yuml_httpclientimpl + +HttpClientImpl + + + +fetch() +yuml_httpclientimpl->yuml_fetchclient + + +yuml_requestclient + +RequestClient + + + +fetch() +fetchRaw() +get() +post() +patch() +delete() +yuml_fetchoptions + +FetchOptions + +method +body + + +yuml_configoptions + +ConfigOptions + +headers +mode +credentials +cache + + +yuml_libraryconfiguration + +LibraryConfiguration + +globalCacheDisable +defaultCachingStore +defaultCachingTimeoutSeconds +enableCacheExpiration +cacheExpirationIntervalMilliseconds +spfxContext + + +yuml_runtimeconfigimpl + +RuntimeConfigImpl + +_v +defaultCachingStore +defaultCachingTimeoutSeconds +globalCacheDisable +enableCacheExpiration +cacheExpirationIntervalMilliseconds +spfxContext + +extend() +get() +yuml_error + +Error +yuml_urlexception + +UrlException + + + + +yuml_error->yuml_urlexception + + +yuml_typedhash + +TypedHash + + + + +yuml_dictionary + +Dictionary + +keys +values +count + +get() +add() +merge() +remove() +getKeys() +getValues() +clear() + + diff --git a/v1/documentation/img/pnpjs-config-store-uml.svg b/v1/documentation/img/pnpjs-config-store-uml.svg new file mode 100644 index 000000000..533057b6a --- /dev/null +++ b/v1/documentation/img/pnpjs-config-store-uml.svg @@ -0,0 +1,56 @@ + + + + +Gyuml_iconfigurationprovider + +IConfigurationProvider + + + +getConfiguration() +yuml_default + +default + +wrappedProvider +cacheKey +store + +getWrappedProvider() +getConfiguration() +selectPnPCache() +yuml_iconfigurationprovider->yuml_default + + +yuml_iconfigurationprovider->yuml_default + + +yuml_error + +Error +yuml_nocacheavailableexception + +NoCacheAvailableException + + + + +yuml_error->yuml_nocacheavailableexception + + +yuml_settings + +Settings + +_settings + +add() +addJSON() +apply() +load() +get() +getJSON() + + diff --git a/v1/documentation/img/pnpjs-graph-uml.svg b/v1/documentation/img/pnpjs-graph-uml.svg new file mode 100644 index 000000000..99b260e53 --- /dev/null +++ b/v1/documentation/img/pnpjs-graph-uml.svg @@ -0,0 +1,602 @@ + + + + +Gyuml_requestclient + +RequestClient +yuml_graphhttpclient + +GraphHttpClient + +_impl + +fetch() +fetchRaw() +get() +post() +patch() +delete() +yuml_requestclient->yuml_graphhttpclient + + +yuml_error + +Error +yuml_nographclientavailableexception + +NoGraphClientAvailableException + + + + +yuml_error->yuml_nographclientavailableexception + + +yuml_graphbatchparseexception + +GraphBatchParseException + + + + +yuml_error->yuml_graphbatchparseexception + + +yuml_graphconfiguration + +GraphConfiguration + + + + +yuml_graphconfigurationpart + +GraphConfigurationPart + +graph + + +yuml_graphruntimeconfigimpl + +GraphRuntimeConfigImpl + +headers +fetchClientFactory + + +yuml_graphqueryableinstance + +GraphQueryableInstance + + + +select() +expand() +yuml_user + +User + + + + +yuml_graphqueryableinstance->yuml_user + + +yuml_team + +Team + + + +update() +get() +yuml_graphqueryableinstance->yuml_team + + +yuml_plan + +Plan + + + + +yuml_graphqueryableinstance->yuml_plan + + +yuml_photo + +Photo + + + +getBlob() +getBuffer() +setContent() +yuml_graphqueryableinstance->yuml_photo + + +yuml_section + +Section + + + + +yuml_graphqueryableinstance->yuml_section + + +yuml_notebook + +Notebook + +sections + + +yuml_graphqueryableinstance->yuml_notebook + + +yuml_onenote + +OneNote + +notebooks +sections +pages + + +yuml_graphqueryableinstance->yuml_onenote + + +yuml_member + +Member + + + + +yuml_graphqueryableinstance->yuml_member + + +yuml_me + +Me + +onenote + + +yuml_graphqueryableinstance->yuml_me + + +yuml_group + +Group + +calendar +events +owners +plans +members +conversations +acceptedSenders +rejectedSenders +photo +team + +addFavorite() +createTeam() +getMemberGroups() +delete() +update() +removeFavorite() +resetUnseenCount() +subscribeByMail() +unsubscribeByMail() +getCalendarView() +yuml_graphqueryableinstance->yuml_group + + +yuml_post + +Post + +attachments + +delete() +forward() +reply() +yuml_graphqueryableinstance->yuml_post + + +yuml_thread + +Thread + +posts + +reply() +delete() +yuml_graphqueryableinstance->yuml_thread + + +yuml_conversation + +Conversation + +threads + +update() +delete() +yuml_graphqueryableinstance->yuml_conversation + + +yuml_event + +Event + + + +update() +delete() +yuml_graphqueryableinstance->yuml_event + + +yuml_calendar + +Calendar + +events + + +yuml_graphqueryableinstance->yuml_calendar + + +yuml_attachment + +Attachment + + + + +yuml_graphqueryableinstance->yuml_attachment + + +yuml_graphqueryablecollection + +GraphQueryableCollection + +count + +filter() +select() +expand() +orderBy() +top() +skip() +skipToken() +yuml_users + +Users + + + +getById() +yuml_graphqueryablecollection->yuml_users + + +yuml_plans + +Plans + + + +getById() +yuml_graphqueryablecollection->yuml_plans + + +yuml_pages + +Pages + + + + +yuml_graphqueryablecollection->yuml_pages + + +yuml_sections + +Sections + + + +getById() +add() +yuml_graphqueryablecollection->yuml_sections + + +yuml_notebooks + +Notebooks + + + +getById() +add() +yuml_graphqueryablecollection->yuml_notebooks + + +yuml_members + +Members + + + +add() +getById() +yuml_graphqueryablecollection->yuml_members + + +yuml_groups + +Groups + + + +getById() +add() +yuml_graphqueryablecollection->yuml_groups + + +yuml_graphqueryablesearchablecollection + +GraphQueryableSearchableCollection + + + +search() +yuml_graphqueryablecollection->yuml_graphqueryablesearchablecollection + + +yuml_senders + +Senders + + + +add() +remove() +yuml_graphqueryablecollection->yuml_senders + + +yuml_posts + +Posts + + + +getById() +add() +yuml_graphqueryablecollection->yuml_posts + + +yuml_threads + +Threads + + + +getById() +add() +yuml_graphqueryablecollection->yuml_threads + + +yuml_conversations + +Conversations + + + +add() +getById() +yuml_graphqueryablecollection->yuml_conversations + + +yuml_events + +Events + + + +getById() +add() +yuml_graphqueryablecollection->yuml_events + + +yuml_calendars + +Calendars + + + + +yuml_graphqueryablecollection->yuml_calendars + + +yuml_attachments + +Attachments + + + +getById() +addFile() +yuml_graphqueryablecollection->yuml_attachments + + +yuml_teamproperties + +TeamProperties + +memberSettings +guestSettings +messagingSettings +funSettings + + +yuml_graphendpoints + +GraphEndpoints + +Beta +V1 + +ensure() +yuml_teamcreateresult + +TeamCreateResult + +data +group +team + + +yuml_teamupdateresult + +TeamUpdateResult + +data +team + + +yuml_teams + +Teams + + + +create() +yuml_graphqueryable + +GraphQueryable + + + +as() +toUrlAndQuery() +getParent() +clone() +setEndpoint() +toRequestContext() +yuml_graphqueryable->yuml_graphqueryableinstance + + +yuml_graphqueryable->yuml_graphqueryablecollection + + +yuml_graphrest + +GraphRest + +groups +teams +me +users + +setup() +yuml_graphqueryable->yuml_graphrest + + +yuml_onenotemethods + +OneNoteMethods + +notebooks +sections +pages + + +yuml_onenotemethods->yuml_onenote + + +yuml_sectionaddresult + +SectionAddResult + +data +section + + +yuml_notebookaddresult + +NotebookAddResult + +data +notebook + + +yuml_owners + +Owners + + + + +yuml_members->yuml_owners + + +yuml_groupaddresult + +GroupAddResult + +group +data + + +yuml_odataqueryable + +ODataQueryable +yuml_odataqueryable->yuml_graphqueryable + + +yuml_graphqueryableconstructor + +GraphQueryableConstructor + + + + +yuml_postforwardinfo + +PostForwardInfo + +comment +toRecipients + + +yuml_eventaddresult + +EventAddResult + +data +event + + +yuml_odatabatch + +ODataBatch +yuml_graphbatch + +GraphBatch + +batchUrl + +executeImpl() +formatRequests() +_parseResponse() +yuml_odatabatch->yuml_graphbatch + + + + diff --git a/v1/documentation/img/pnpjs-logging-uml.svg b/v1/documentation/img/pnpjs-logging-uml.svg new file mode 100644 index 000000000..3a16c31ef --- /dev/null +++ b/v1/documentation/img/pnpjs-logging-uml.svg @@ -0,0 +1,59 @@ + + + + +Gyuml_logger + +Logger + +_instance +activeLogLevel +instance +count + +subscribe() +clearSubscribers() +write() +writeJSON() +log() +error() +yuml_logentry + +LogEntry + +message +level +data + + +yuml_loglistener + +LogListener + + + +log() +yuml_functionlistener + +FunctionListener + +method + +log() +yuml_loglistener->yuml_functionlistener + + +yuml_consolelistener + +ConsoleListener + + + +log() +format() +yuml_loglistener->yuml_consolelistener + + + + diff --git a/v1/documentation/img/pnpjs-nodejs-uml.svg b/v1/documentation/img/pnpjs-nodejs-uml.svg new file mode 100644 index 000000000..2cd7c11da --- /dev/null +++ b/v1/documentation/img/pnpjs-nodejs-uml.svg @@ -0,0 +1,85 @@ + + + + +Gyuml_httpclientimpl + +HttpClientImpl +yuml_spfetchclient + +SPFetchClient + +siteUrl +_clientId +_clientSecret +authEnv +_realm +SharePointServicePrincipal +token + +fetch() +getAddInOnlyAccessToken() +getAuthHostUrl() +getRealm() +getAuthUrl() +getFormattedPrincipal() +toDate() +yuml_httpclientimpl->yuml_spfetchclient + + +yuml_adalfetchclient + +AdalFetchClient + +_tenant +_clientId +_secret +_resource +_authority +authContext + +fetch() +acquireToken() +yuml_httpclientimpl->yuml_adalfetchclient + + +yuml_authtoken + +AuthToken + +token_type +expires_in +not_before +expires_on +resource +access_token + + +yuml_aadtoken + +AADToken + +accessToken +expiresIn +expiresOn +isMRRT +resource +tokenType + + +yuml_error + +Error +yuml_authurlexception + +AuthUrlException + + + + +yuml_error->yuml_authurlexception + + + + diff --git a/v1/documentation/img/pnpjs-odata-uml.svg b/v1/documentation/img/pnpjs-odata-uml.svg new file mode 100644 index 000000000..fd1726a34 --- /dev/null +++ b/v1/documentation/img/pnpjs-odata-uml.svg @@ -0,0 +1,253 @@ + + + + +Gyuml_queryable + +Queryable + +_options +_query +_url +_parentUrl +_useCaching +_cachingOptions +query +parentUrl + +toUrlAndQuery() +toUrl() +concat() +configure() +configureFrom() +usingCaching() +getCore() +postCore() +patchCore() +deleteCore() +putCore() +append() +extend() +toRequestContext() +yuml_odataqueryable + +ODataQueryable + +_batch +hasBatch +batch + +inBatch() +toUrl() +get() +getCore() +postCore() +patchCore() +deleteCore() +putCore() +addBatchDependency() +yuml_queryable->yuml_odataqueryable + + +yuml_error + +Error +yuml_alreadyinbatchexception + +AlreadyInBatchException + + + + +yuml_error->yuml_alreadyinbatchexception + + +yuml_processhttpclientresponseexception + +ProcessHttpClientResponseException + +status +statusText +data + + +yuml_error->yuml_processhttpclientresponseexception + + +yuml_requestcontext + +RequestContext + +batch +batchDependency +cachingOptions +hasResult +isBatched +isCached +options +parser +pipeline +requestAbsoluteUrl +requestId +result +verb +clientFactory + + +yuml_pipelinemethods + +PipelineMethods + + + +logStart() +caching() +send() +logEnd() +yuml_odataparser + +ODataParser + +hydrate + +parse() +yuml_lambdaparser + +LambdaParser + +parser + +parse() +yuml_odataparser->yuml_lambdaparser + + +yuml_odataparserbase + +ODataParserBase + + + +parse() +parseImpl() +handleError() +parseODataJSON() +yuml_odataparser->yuml_odataparserbase + + +yuml_cachingparserwrapper + +CachingParserWrapper + +parser +cacheOptions + +parse() +cacheData() +yuml_odataparser->yuml_cachingparserwrapper + + +yuml_bufferparser + +BufferParser + + + +parseImpl() +yuml_odataparserbase->yuml_bufferparser + + +yuml_jsonparser + +JSONParser + + + +parseImpl() +yuml_odataparserbase->yuml_jsonparser + + +yuml_blobparser + +BlobParser + + + +parseImpl() +yuml_odataparserbase->yuml_blobparser + + +yuml_textparser + +TextParser + + + +parseImpl() +yuml_odataparserbase->yuml_textparser + + +yuml_odatadefaultparser + +ODataDefaultParser + + + + +yuml_odataparserbase->yuml_odatadefaultparser + + +yuml_odatabatchrequestinfo + +ODataBatchRequestInfo + +url +method +options +parser +resolve +reject +id + + +yuml_odatabatch + +ODataBatch + +_batchId +_dependencies +_requests +_resolveBatchDependencies +batchId +requests + +add() +addDependency() +addResolveBatchDependency() +execute() +executeImpl() +yuml_icachingoptions + +ICachingOptions + +expiration +storeName +key + + +yuml_cachingoptions + +CachingOptions + +key +storage +expiration +storeName +store + + +yuml_icachingoptions->yuml_cachingoptions + + + + diff --git a/v1/documentation/img/pnpjs-sp-addinhelpers-uml.svg b/v1/documentation/img/pnpjs-sp-addinhelpers-uml.svg new file mode 100644 index 000000000..23ce95ab5 --- /dev/null +++ b/v1/documentation/img/pnpjs-sp-addinhelpers-uml.svg @@ -0,0 +1,48 @@ + + + + +Gyuml_sprest + +SPRest +yuml_sprestaddin + +SPRestAddIn + + + +crossDomainSite() +crossDomainWeb() +_cdImpl() +yuml_sprest->yuml_sprestaddin + + +yuml_httpclientimpl + +HttpClientImpl +yuml_sprequestexecutorclient + +SPRequestExecutorClient + +convertToResponse + +fetch() +yuml_httpclientimpl->yuml_sprequestexecutorclient + + +yuml_error + +Error +yuml_sprequestexecutorundefinedexception + +SPRequestExecutorUndefinedException + + + + +yuml_error->yuml_sprequestexecutorundefinedexception + + + + diff --git a/v1/documentation/img/pnpjs-sp-clientsvc-uml.svg b/v1/documentation/img/pnpjs-sp-clientsvc-uml.svg new file mode 100644 index 000000000..5d4be5a9b --- /dev/null +++ b/v1/documentation/img/pnpjs-sp-clientsvc-uml.svg @@ -0,0 +1,167 @@ + + + + +Gyuml_processqueryparser + +ProcessQueryParser + +op + +parse() +findResult() +getParsedResultById() +yuml_imethodparamsbuilder + +IMethodParamsBuilder + + + +string() +number() +boolean() +objectPath() +toArray() +yuml_methodparams + +MethodParams + +_p + +build() +string() +number() +boolean() +objectPath() +toArray() +a() +yuml_imethodparamsbuilder->yuml_methodparams + + +yuml_iobjectpath + +IObjectPath + +path +actions +id + + +yuml_objectpath + +ObjectPath + +path +actions +id +replaceAfter + + +yuml_iobjectpath->yuml_objectpath + + +yuml_objectpathqueue + +ObjectPathQueue + +_paths +_relationships +_xml +_contextIndex +_siteIndex +_webIndex +last +lastIndex +siteIndex +webIndex +contextIndex + +add() +addChildRelationship() +getChildRelationship() +getChildRelationships() +appendAction() +appendActionToLast() +clone() +toArray() +hash() +toBody() +toIndexedTree() +dirty() +yuml_iclientsvcqueryable + +IClientSvcQueryable + + + +select() +usingCaching() +inBatch() +yuml_clientsvcqueryable + +ClientSvcQueryable + +_objectPaths +_selects +_batch +hasBatch +batch + +select() +inBatch() +toUrlAndQuery() +getSelects() +getChild() +getChildProperty() +send() +sendGet() +sendGetCollection() +invokeMethod() +invokeNonQuery() +invokeMethodCollection() +invokeUpdate() +toRequestContext() +addBatchDependency() +invokeMethodImpl() +yuml_iclientsvcqueryable->yuml_clientsvcqueryable + + +yuml_queryable + +Queryable +yuml_queryable->yuml_clientsvcqueryable + + +yuml_clientsvcqueryableconstructor + +ClientSvcQueryableConstructor + + + + +yuml_iobjectpathbatch + +IObjectPathBatch + + + + +yuml_objectpathbatch + +ObjectPathBatch + +parentUrl + +executeImpl() +yuml_iobjectpathbatch->yuml_objectpathbatch + + +yuml_odatabatch + +ODataBatch +yuml_odatabatch->yuml_objectpathbatch + + + + diff --git a/v1/documentation/img/pnpjs-sp-taxonomy-uml.svg b/v1/documentation/img/pnpjs-sp-taxonomy-uml.svg new file mode 100644 index 000000000..5944a31d3 --- /dev/null +++ b/v1/documentation/img/pnpjs-sp-taxonomy-uml.svg @@ -0,0 +1,458 @@ + + + + +Gyuml_changeinformation + +ChangeInformation + +ItemType +OperationType +StartTime +WithinTimeSpan + + +yuml_changeditem + +ChangedItem + +ChangedBy +ChangedTime +Id +ItemType +Operation +SiteId +TermId +ChangedCustomProperties +ChangedLocalCustomProperties +LcidsForChangedDescriptions +LcidsForChangedLabels +TermSetId +FromGroupId +GroupId +ChangedLanguage +IsDefaultLanguageChanged +IsFullFarmRestore + + +yuml_ilabelmatchinfo + +ILabelMatchInfo + +DefaultLabelOnly +ExcludeKeyword +Lcid +ResultCollectionSize +StringMatchOption +TermLabel +TrimDeprecated +TrimUnavailable + + +yuml_timespan + +TimeSpan + +Days +Hours +Milliseconds +Minutes +Seconds +Ticks +TotalDays +TotalHours +TotalMilliseconds +TotalMinutes +TotalSeconds + + +yuml_itermstore + +ITermStore + +hashTagsTermSet +keywordsTermSet +orphanedTermsTermSet +systemGroup + +addGroup() +addLanguage() +commitAll() +deleteLanguage() +get() +getChanges() +getSiteCollectionGroup() +getTermById() +getTermInTermSet() +getTermGroupById() +getTerms() +getTermSetById() +getTermSetsByName() +rollbackAll() +update() +updateCache() +updateUsedTermsOnSite() +yuml_termstore + +TermStore + +hashTagsTermSet +keywordsTermSet +orphanedTermsTermSet +systemGroup + +get() +getTermSetsByName() +getTermSetById() +getTermById() +getTermInTermSet() +getTermGroupById() +getTerms() +getSiteCollectionGroup() +addLanguage() +addGroup() +commitAll() +deleteLanguage() +rollbackAll() +updateCache() +update() +updateUsedTermsOnSite() +getChanges() +yuml_itermstore->yuml_termstore + + +yuml_clientsvcqueryable + +ClientSvcQueryable +yuml_clientsvcqueryable->yuml_termstore + + +yuml_termstores + +TermStores + + + +get() +getByName() +getById() +yuml_clientsvcqueryable->yuml_termstores + + +yuml_termset + +TermSet + +group +terms + +addStakeholder() +deleteStakeholder() +get() +getTermById() +addTerm() +copy() +update() +yuml_clientsvcqueryable->yuml_termset + + +yuml_termsets + +TermSets + + + +get() +getById() +getByName() +yuml_clientsvcqueryable->yuml_termsets + + +yuml_term + +Term + +labels +parent +pinSourceTermSet +reusedTerms +sourceTerm +termSet +termSets + +createLabel() +deprecate() +get() +setDescription() +setLocalCustomProperty() +update() +yuml_clientsvcqueryable->yuml_term + + +yuml_terms + +Terms + + + +get() +getById() +getByName() +yuml_clientsvcqueryable->yuml_terms + + +yuml_termgroup + +TermGroup + +store +termSets + +addContributor() +addGroupManager() +createTermSet() +get() +update() +yuml_clientsvcqueryable->yuml_termgroup + + +yuml_session + +Session + +termStores + +setup() +createBatch() +getDefaultKeywordTermStore() +getDefaultSiteCollectionTermStore() +yuml_clientsvcqueryable->yuml_session + + +yuml_label + +Label + + + +get() +setAsDefaultForLanguage() +delete() +yuml_clientsvcqueryable->yuml_label + + +yuml_labels + +Labels + + + +getByValue() +get() +yuml_clientsvcqueryable->yuml_labels + + +yuml_itermstores + +ITermStores + + + +get() +getByName() +getById() +yuml_itermstores->yuml_termstores + + +yuml_itermstoredata + +ITermStoreData + +DefaultLanguage +Id +IsOnline +Languages +Name +WorkingLanguage + + +yuml_itermset + +ITermSet + +terms +group + +copy() +get() +getTermById() +addTerm() +update() +yuml_itermset->yuml_termset + + +yuml_itermsets + +ITermSets + + + +getById() +getByName() +get() +yuml_itermsets->yuml_termsets + + +yuml_itermsetdata + +ITermSetData + +Contact +CreatedDate +CustomProperties +CustomSortOrder +Description +Id +IsAvailableForTagging +IsOpenForTermCreation +LastModifiedDate +Name +Names +Owner +Stakeholders + + +yuml_iterm + +ITerm + +labels +parent +pinSourceTermSet +reusedTerms +sourceTerm +termSet +termSets + +createLabel() +deprecate() +get() +setDescription() +setLocalCustomProperty() +update() +yuml_iterm->yuml_term + + +yuml_iterms + +ITerms + + + +get() +getById() +getByName() +yuml_iterms->yuml_terms + + +yuml_itermdata + +ITermData + +CustomProperties +CustomSortOrder +Description +Id +IsAvailableForTagging +IsDeprecated +IsKeyword +IsPinned +IsPinnedRoot +IsReused +IsRoot +IsSourceTerm +LastModifiedDate +LocalCustomProperties +MergedTermIds +Name +Owner +PathOfTerm +TermsCount + + +yuml_itermgroup + +ITermGroup + +store +termSets + +addContributor() +addGroupManager() +createTermSet() +get() +update() +yuml_itermgroup->yuml_termgroup + + +yuml_itermgroupdata + +ITermGroupData + +CreatedDate +Description +Id +IsSiteCollectionGroup +IsSystemGroup +LastModifiedDate +Name + + +yuml_itaxonomysession + +ITaxonomySession + +termStores + +setup() +createBatch() +getDefaultKeywordTermStore() +getDefaultSiteCollectionTermStore() +yuml_itaxonomysession->yuml_session + + +yuml_ilabel + +ILabel + + + +get() +setAsDefaultForLanguage() +delete() +yuml_ilabel->yuml_label + + +yuml_ilabels + +ILabels + + + +getByValue() +get() +yuml_ilabels->yuml_labels + + +yuml_ilabeldata + +ILabelData + +IsDefaultForLanguage +Language +Value + + + + diff --git a/v1/documentation/img/pnpjs-sp-uml.svg b/v1/documentation/img/pnpjs-sp-uml.svg new file mode 100644 index 000000000..f71d83e0a --- /dev/null +++ b/v1/documentation/img/pnpjs-sp-uml.svg @@ -0,0 +1,2833 @@ + + + + +Gyuml_requestclient + +RequestClient +yuml_sphttpclient + +SPHttpClient + +_digestCache +_impl + +fetch() +fetchRaw() +get() +post() +patch() +delete() +yuml_requestclient->yuml_sphttpclient + + +yuml_digestcache + +DigestCache + +_httpClient +_digests + +getDigest() +clear() +yuml_cacheddigest + +CachedDigest + +expiration +value + + +yuml_spconfiguration + +SPConfiguration + + + + +yuml_spconfigurationpart + +SPConfigurationPart + +sp + + +yuml_spruntimeconfigimpl + +SPRuntimeConfigImpl + +headers +baseUrl +fetchClientFactory + + +yuml_sharepointqueryableshareableweb + +SharePointQueryableShareableWeb + + + +shareWith() +shareObject() +shareObjectRaw() +unshareObject() +yuml_web + +Web + +webs +allProperties +webinfos +contentTypes +lists +fields +features +availablefields +navigation +siteUsers +siteGroups +siteUserInfoList +regionalSettings +currentUser +folders +userCustomActions +roleDefinitions +relatedItems +rootFolder +associatedOwnerGroup +associatedMemberGroup +associatedVisitorGroup +customListTemplate + +fromUrl() +getParentWeb() +getSubwebsFilteredForCurrentUser() +createBatch() +getFolderByServerRelativeUrl() +getFolderByServerRelativePath() +getFileByServerRelativeUrl() +getFileByServerRelativePath() +getList() +update() +delete() +applyTheme() +applyWebTemplate() +ensureUser() +availableWebTemplates() +getCatalog() +getChanges() +getUserById() +mapToIcon() +getStorageEntity() +setStorageEntity() +removeStorageEntity() +getAppCatalog() +getClientSideWebParts() +addClientSidePage() +addClientSidePageByPath() +yuml_sharepointqueryableshareableweb->yuml_web + + +yuml_sharepointqueryablecollection + +SharePointQueryableCollection + + + +filter() +select() +expand() +orderBy() +skip() +top() +yuml_webinfos + +WebInfos + + + + +yuml_sharepointqueryablecollection->yuml_webinfos + + +yuml_webs + +Webs + + + +add() +yuml_sharepointqueryablecollection->yuml_webs + + +yuml_webpartdefinitions + +WebPartDefinitions + + + +getById() +getByControlId() +yuml_sharepointqueryablecollection->yuml_webpartdefinitions + + +yuml_viewfields + +ViewFields + + + +getSchemaXml() +add() +move() +removeAll() +remove() +yuml_sharepointqueryablecollection->yuml_viewfields + + +yuml_views + +Views + + + +getById() +getByTitle() +add() +yuml_sharepointqueryablecollection->yuml_views + + +yuml_usercustomactions + +UserCustomActions + + + +getById() +add() +clear() +yuml_sharepointqueryablecollection->yuml_usercustomactions + + +yuml_subscriptions + +Subscriptions + + + +getById() +add() +yuml_sharepointqueryablecollection->yuml_subscriptions + + +yuml_siteusers + +SiteUsers + + + +getByEmail() +getById() +getByLoginName() +removeById() +removeByLoginName() +add() +yuml_sharepointqueryablecollection->yuml_siteusers + + +yuml_sitegroups + +SiteGroups + + + +add() +getByName() +getById() +removeById() +removeByLoginName() +yuml_sharepointqueryablecollection->yuml_sitegroups + + +yuml_roledefinitionbindings + +RoleDefinitionBindings + + + + +yuml_sharepointqueryablecollection->yuml_roledefinitionbindings + + +yuml_roledefinitions + +RoleDefinitions + + + +getById() +getByName() +getByType() +add() +yuml_sharepointqueryablecollection->yuml_roledefinitions + + +yuml_roleassignments + +RoleAssignments + + + +add() +remove() +getById() +yuml_sharepointqueryablecollection->yuml_roleassignments + + +yuml_timezones + +TimeZones + + + +getById() +yuml_sharepointqueryablecollection->yuml_timezones + + +yuml_installedlanguages + +InstalledLanguages + + + + +yuml_sharepointqueryablecollection->yuml_installedlanguages + + +yuml_navigationnodes + +NavigationNodes + + + +getById() +add() +moveAfter() +yuml_sharepointqueryablecollection->yuml_navigationnodes + + +yuml_lists + +Lists + + + +getByTitle() +getById() +add() +ensure() +ensureSiteAssetsLibrary() +ensureSitePagesLibrary() +yuml_sharepointqueryablecollection->yuml_lists + + +yuml_itemversions + +ItemVersions + + + +getById() +yuml_sharepointqueryablecollection->yuml_itemversions + + +yuml_items + +Items + + + +getById() +getItemByStringId() +skip() +getPaged() +getAll() +add() +ensureListItemEntityTypeName() +yuml_sharepointqueryablecollection->yuml_items + + +yuml_forms + +Forms + + + +getById() +yuml_sharepointqueryablecollection->yuml_forms + + +yuml_folders + +Folders + + + +getByName() +add() +yuml_sharepointqueryablecollection->yuml_folders + + +yuml_versions + +Versions + + + +getById() +deleteAll() +deleteById() +recycleByID() +deleteByLabel() +recycleByLabel() +restoreByLabel() +yuml_sharepointqueryablecollection->yuml_versions + + +yuml_files + +Files + + + +getByName() +add() +addChunked() +addTemplateFile() +yuml_sharepointqueryablecollection->yuml_files + + +yuml_fields + +Fields + + + +getByTitle() +getByInternalNameOrTitle() +getById() +createFieldAsXml() +add() +addText() +addCalculated() +addDateTime() +addNumber() +addCurrency() +addMultilineText() +addUrl() +addUser() +addLookup() +addChoice() +addMultiChoice() +addBoolean() +yuml_sharepointqueryablecollection->yuml_fields + + +yuml_features + +Features + + + +getById() +add() +remove() +yuml_sharepointqueryablecollection->yuml_features + + +yuml_fieldlinks + +FieldLinks + + + +getById() +yuml_sharepointqueryablecollection->yuml_fieldlinks + + +yuml_contenttypes + +ContentTypes + + + +getById() +addAvailableContentType() +add() +yuml_sharepointqueryablecollection->yuml_contenttypes + + +yuml_replies + +Replies + + + +add() +yuml_sharepointqueryablecollection->yuml_replies + + +yuml_comments + +Comments + + + +getById() +add() +clear() +yuml_sharepointqueryablecollection->yuml_comments + + +yuml_attachmentfiles + +AttachmentFiles + + + +getByName() +add() +addMultiple() +deleteMultiple() +yuml_sharepointqueryablecollection->yuml_attachmentfiles + + +yuml_appcatalog + +AppCatalog + + + +getAppById() +add() +yuml_sharepointqueryablecollection->yuml_appcatalog + + +yuml_webensureuserresult + +WebEnsureUserResult + +data +user + + +yuml_getcatalogresult + +GetCatalogResult + +data +list + + +yuml_webupdateresult + +WebUpdateResult + +data +web + + +yuml_webaddresult + +WebAddResult + +data +web + + +yuml_sharepointqueryableinstance + +SharePointQueryableInstance + + + +select() +expand() +yuml_webpart + +WebPart + + + + +yuml_sharepointqueryableinstance->yuml_webpart + + +yuml_webpartdefinition + +WebPartDefinition + +webpart + +saveChanges() +moveTo() +close() +open() +delete() +yuml_sharepointqueryableinstance->yuml_webpartdefinition + + +yuml_view + +View + +fields + +update() +delete() +renderAsHtml() +yuml_sharepointqueryableinstance->yuml_view + + +yuml_userprofilequery + +UserProfileQuery + +clientPeoplePickerQuery +profileLoader +editProfileLink +isMyPeopleListPublic +myFollowers +myProperties +trendingTags +ownerUserProfile +userProfile + +amIFollowedBy() +amIFollowing() +getFollowedTags() +getFollowersFor() +getPeopleFollowedBy() +getPropertiesFor() +getUserProfilePropertyFor() +hideSuggestion() +isFollowing() +setMyProfilePic() +setSingleValueProfileProperty() +setMultiValuedProfileProperty() +createPersonalSiteEnqueueBulk() +createPersonalSite() +shareAllSocialData() +clientPeoplePickerResolveUser() +clientPeoplePickerSearchUser() +yuml_sharepointqueryableinstance->yuml_userprofilequery + + +yuml_usercustomaction + +UserCustomAction + + + +update() +delete() +yuml_sharepointqueryableinstance->yuml_usercustomaction + + +yuml_subscription + +Subscription + + + +update() +delete() +yuml_sharepointqueryableinstance->yuml_subscription + + +yuml_mysocialquery + +MySocialQuery + + + +followed() +followedCount() +followers() +suggestions() +yuml_sharepointqueryableinstance->yuml_mysocialquery + + +yuml_socialquery + +SocialQuery + +my + +getFollowedSitesUri() +getFollowedDocumentsUri() +follow() +isFollowed() +stopFollowing() +createSocialActorInfoRequestBody() +yuml_sharepointqueryableinstance->yuml_socialquery + + +yuml_currentuser + +CurrentUser + + + + +yuml_sharepointqueryableinstance->yuml_currentuser + + +yuml_siteuser + +SiteUser + +groups + +update() +delete() +yuml_sharepointqueryableinstance->yuml_siteuser + + +yuml_sitegroup + +SiteGroup + +users + +update() +yuml_sharepointqueryableinstance->yuml_sitegroup + + +yuml_site + +Site + +rootWeb +features +userCustomActions + +getRootWeb() +getContextInfo() +getDocumentLibraries() +getWebUrlFromPageUrl() +createBatch() +openWebById() +yuml_sharepointqueryableinstance->yuml_site + + +yuml_filefoldershared + +FileFolderShared + + + +getShareLink() +checkSharingPermissions() +getSharingInformation() +getObjectSharingSettings() +unshare() +deleteSharingLinkByKind() +unshareLink() +getShareable() +yuml_sharepointqueryableinstance->yuml_filefoldershared + + +yuml_sharepointqueryablesecurable + +SharePointQueryableSecurable + +roleAssignments +firstUniqueAncestorSecurableObject + +getUserEffectivePermissions() +getCurrentUserEffectivePermissions() +breakRoleInheritance() +resetRoleInheritance() +userHasPermissions() +currentUserHasPermissions() +hasPermissions() +yuml_sharepointqueryableinstance->yuml_sharepointqueryablesecurable + + +yuml_searchsuggest + +SearchSuggest + + + +execute() +mapQueryToQueryString() +yuml_sharepointqueryableinstance->yuml_searchsuggest + + +yuml_search + +Search + + + +execute() +fixupProp() +yuml_sharepointqueryableinstance->yuml_search + + +yuml_roledefinition + +RoleDefinition + + + +update() +delete() +yuml_sharepointqueryableinstance->yuml_roledefinition + + +yuml_roleassignment + +RoleAssignment + +groups +bindings + +delete() +yuml_sharepointqueryableinstance->yuml_roleassignment + + +yuml_timezone + +TimeZone + + + +utcToLocalTime() +localTimeToUTC() +yuml_sharepointqueryableinstance->yuml_timezone + + +yuml_regionalsettings + +RegionalSettings + +installedLanguages +globalInstalledLanguages +timeZone +timeZones + + +yuml_sharepointqueryableinstance->yuml_regionalsettings + + +yuml_navigationnode + +NavigationNode + +children + +delete() +yuml_sharepointqueryableinstance->yuml_navigationnode + + +yuml_itemversion + +ItemVersion + + + +delete() +yuml_sharepointqueryableinstance->yuml_itemversion + + +yuml_form + +Form + + + + +yuml_sharepointqueryableinstance->yuml_form + + +yuml_version + +Version + + + +delete() +yuml_sharepointqueryableinstance->yuml_version + + +yuml_field + +Field + + + +update() +delete() +setShowInDisplayForm() +setShowInEditForm() +setShowInNewForm() +yuml_sharepointqueryableinstance->yuml_field + + +yuml_feature + +Feature + + + +deactivate() +yuml_sharepointqueryableinstance->yuml_feature + + +yuml_fieldlink + +FieldLink + + + + +yuml_sharepointqueryableinstance->yuml_fieldlink + + +yuml_contenttype + +ContentType + +fieldLinks +fields +parent +workflowAssociations + +delete() +yuml_sharepointqueryableinstance->yuml_contenttype + + +yuml_comment + +Comment + +replies + +like() +unlike() +delete() +yuml_sharepointqueryableinstance->yuml_comment + + +yuml_attachmentfile + +AttachmentFile + + + +getText() +getBlob() +getBuffer() +getJSON() +setContent() +delete() +getParsed() +yuml_sharepointqueryableinstance->yuml_attachmentfile + + +yuml_app + +App + + + +deploy() +retract() +install() +uninstall() +upgrade() +remove() +yuml_sharepointqueryableinstance->yuml_app + + +yuml_sharepointqueryable + +SharePointQueryable + + + +as() +toUrlAndQuery() +getParent() +clone() +toRequestContext() +yuml_sharepointqueryable->yuml_sharepointqueryablecollection + + +yuml_sharepointqueryable->yuml_sharepointqueryableinstance + + +yuml_limitedwebpartmanager + +LimitedWebPartManager + +webparts + +export() +import() +yuml_sharepointqueryable->yuml_limitedwebpartmanager + + +yuml_utilitymethod + +UtilityMethod + + + +getBaseUrl() +excute() +sendEmail() +getCurrentUserEmailAddresses() +resolvePrincipal() +searchPrincipals() +createEmailBodyForInvitation() +expandGroupsToPrincipals() +createWikiPage() +yuml_sharepointqueryable->yuml_utilitymethod + + +yuml_sharepointqueryableshareable + +SharePointQueryableShareable + + + +getShareLink() +shareWith() +shareObject() +unshareObjectWeb() +checkPermissions() +getSharingInformation() +getObjectSharingSettings() +unshareObject() +deleteLinkByKind() +unshareLink() +getRoleValue() +getShareObjectWeb() +sendShareObjectRequest() +yuml_sharepointqueryable->yuml_sharepointqueryableshareable + + +yuml_relateditemmanagerimpl + +RelatedItemManagerImpl + + + +FromUrl() +getRelatedItems() +getPageOneRelatedItems() +addSingleLink() +addSingleLinkToUrl() +addSingleLinkFromUrl() +deleteSingleLink() +yuml_sharepointqueryable->yuml_relateditemmanagerimpl + + +yuml_navigationservice + +NavigationService + + + +getMenuState() +getMenuNodeKey() +yuml_sharepointqueryable->yuml_navigationservice + + +yuml_navigation + +Navigation + +quicklaunch +topNavigationBar + + +yuml_sharepointqueryable->yuml_navigation + + +yuml_viewupdateresult + +ViewUpdateResult + +view +data + + +yuml_viewaddresult + +ViewAddResult + +view +data + + +yuml_utilitymethods + +UtilityMethods + + + +usingCaching() +inBatch() +sendEmail() +getCurrentUserEmailAddresses() +resolvePrincipal() +searchPrincipals() +createEmailBodyForInvitation() +expandGroupsToPrincipals() +createWikiPage() +yuml_utilitymethods->yuml_utilitymethod + + +yuml_createwikipageresult + +CreateWikiPageResult + +data +file + + +yuml_usercustomactionupdateresult + +UserCustomActionUpdateResult + +data +action + + +yuml_usercustomactionaddresult + +UserCustomActionAddResult + +data +action + + +yuml_likedata + +LikeData + +name +loginName +id +email +creationDate + + +yuml_storageentity + +StorageEntity + +Value +Comment +Description + + +yuml_peoplepickerentitydata + +PeoplePickerEntityData + +AccountName +Department +Email +IsAltSecIdPresent +MobilePhone +ObjectId +OtherMails +PrincipalType +SPGroupID +SPUserID +Title + + +yuml_peoplepickerentity + +PeoplePickerEntity + +Description +DisplayText +EntityData +EntityType +IsResolved +Key +MultipleMatches +ProviderDisplayName +ProviderName + + +yuml_peoplepickerquerysettings + +PeoplePickerQuerySettings + +ExcludeAllUsersOnTenantClaim + + +yuml_clientpeoplepickerqueryparameters + +ClientPeoplePickerQueryParameters + +AllowEmailAddresses +AllowMultipleEntities +AllowOnlyEmailAddresses +AllUrlZones +EnabledClaimProviders +ForceClaims +MaximumEntitySuggestions +PrincipalSource +PrincipalType +QuerySettings +QueryString +SharePointGroupID +UrlZone +UrlZoneSpecified +WebApplicationID + + +yuml_fieldcreationproperties + +FieldCreationProperties + +DefaultFormula +Description +EnforceUniqueValues +FieldTypeKind +Group +Hidden +Indexed +Required +Title +ValidationFormula +ValidationMessage + + +yuml_menunodecollection + +MenuNodeCollection + +FriendlyUrlPrefix +Nodes +SimpleUrl +SPSitePrefix +SPWebPrefix +StartingNodeKey +StartingNodeTitle +Version + + +yuml_menunode + +MenuNode + +CustomProperties +FriendlyUrlSegment +IsDeleted +IsHidden +Key +Nodes +NodeType +SimpleUrl +Title + + +yuml_renderlistdataparameters + +RenderListDataParameters + +AllowMultipleValueFilterForTaxonomyFields +DatesInUtc +ExpandGroups +FirstGroupOnly +FolderServerRelativeUrl +ImageFieldsToTryRewriteToCdnUrls +OverrideViewXml +Paging +RenderOptions +ReplaceGroup +ViewXml + + +yuml_wikipagecreationinformation + +WikiPageCreationInformation + +ServerRelativeUrl +WikiHtmlContent + + +yuml_emailproperties + +EmailProperties + +To +CC +BCC +Subject +Body +AdditionalHeaders +From + + +yuml_sharinginformation + +SharingInformation + +canAddExternalPrincipal +canAddInternalPrincipal +canSendEmail +canUseSimplifiedRoles +hasUniquePermissions +currentRole +requiresAccessApproval +hasPendingAccessRequests +pendingAccessRequestsLink +sharedObjectType +directUrl +webUrl +defaultLinkKind +domainRestrictionMode +RestrictedDomains +anonymousLinkExpirationRestrictionDays +permissionsInformation +pickerSettings + + +yuml_objectsharingsettings + +ObjectSharingSettings + +WebUrl +ListId +ItemId +ItemName +ItemUrl +ObjectSharingInformation +AccessRequestMode +PermissionsOnlyMode +InheritingWebLink +ShareByEmailEnabled +IsGuestUser +HasEditRole +HasReadRole +IsPictureLibrary +CanShareFolder +CanSendEmail +DefaultShareLinkType +SupportsAclPropagation +CanCurrentUserShareInternally +CanCurrentUserShareExternally +CanCurrentUserRetrieveReadonlyLink +CanCurrentUserManageReadonlyLink +CanCurrentUserRetrieveReadWriteLink +CanCurrentUserManageReadWriteLink +CanCurrentUserRetrieveOrganizationReadonlyLink +CanCurrentUserManageOrganizationReadonlyLink +CanCurrentUserRetrieveOrganizationReadWriteLink +CanCurrentUserManageOrganizationReadWriteLink +CanSendLink +ShowExternalSharingWarning +SharingPermissions +SimplifiedRoles +GroupsList +Roles +SharePointSettings +IsUserSiteAdmin +RequiredAnonymousLinkExpirationInDays + + +yuml_sharinginformationrequest + +SharingInformationRequest + +maxPrincipalsToReturn +clientSupportedFeatures + + +yuml_sharingentitypermission + +SharingEntityPermission + +inputEntity +resolvedEntity +hasAccess +role + + +yuml_sharingrecipient + +SharingRecipient + +email +alias + + +yuml_spinvitationcreationresult + +SPInvitationCreationResult + +Succeeded +Email +InvitationLink + + +yuml_usersharingresult + +UserSharingResult + +IsUserKnown +Status +Message +User +DisplayName +Email +CurrentRole +AllowedRoles +InvitationLink + + +yuml_sharingresult + +SharingResult + +PermissionsPageRelativeUrl +UsersWithAccessRequests +StatusCode +ErrorMessage +UniquelyPermissionedUsers +GroupsSharedWith +GroupUsersAddedTo +UsersAddedToGroup +InvitedUsers +Name +Url +IconUrl + + +yuml_sharinglinkinfo + +SharingLinkInfo + +AllowsAnonymousAccess +Created +CreatedBy +Expiration +IsActive +IsEditLink +IsFormsLink +IsUnhealthy +LastModified +LastModifiedBy +LinkKind +ShareId +Url + + +yuml_sharelinkresponse + +ShareLinkResponse + +sharingLinkInfo + + +yuml_sharelinkrequest + +ShareLinkRequest + +peoplePickerInput +createLink +emailData +settings + + +yuml_sharelinksettings + +ShareLinkSettings + +shareId +linkKind +expiration +role +allowAnonymousAccess + + +yuml_sharingemaildata + +SharingEmailData + +subject +body + + +yuml_shareobjectoptions + +ShareObjectOptions + +url +loginNames +role +emailData +group +propagateAcl +includeAnonymousLinkInEmail +useSimplifiedRoles + + +yuml_listformdata + +ListFormData + +ContentType +Title +Author +Editor +Created +Modified +Attachments +ListSchema +FormControlMode +FieldControlModes +WebAttributes +ItemAttributes +ListAttributes +CSRCustomLayout +PostBackRequired +PreviousPostBackHandled +UploadMode +SubmitButtonID +ItemContentTypeName +ItemContentTypeId +JSLinks + + +yuml_renderlistdata + +RenderListData + +Row +FirstRow +FolderPermissions +LastRow +FilterLink +ForceNoHierarchy +HierarchyHasIndention + + +yuml_contextinfo + +ContextInfo + +FormDigestTimeoutSeconds +FormDigestValue +LibraryVersion +SiteFullUrl +SupportedSchemaVersions +WebFullUrl + + +yuml_documentlibraryinformation + +DocumentLibraryInformation + +AbsoluteUrl +Modified +ModifiedFriendlyDisplay +ServerRelativeUrl +Title + + +yuml_principalinfo + +PrincipalInfo + +Department +DisplayName +Email +JobTitle +LoginName +Mobile +PrincipalId +PrincipalType +SIPAddress + + +yuml_useridinfo + +UserIdInfo + +NameId +NameIdIssuer + + +yuml_hashtagcollection + +HashTagCollection + +Items + + +yuml_hashtag + +HashTag + +Name +UseCount + + +yuml_userprofile + +UserProfile + +FollowedContent +AccountName +DisplayName +O15FirstRunExperience +PersonalSite +PersonalSiteCapabilities +PersonalSiteFirstCreationError +PersonalSiteFirstCreationTime +PersonalSiteInstantiationState +PersonalSiteLastCreationTime +PersonalSiteNumberOfRetries +PictureImportEnabled +PublicUrl +UrlToCreatePersonalSite + + +yuml_followedcontent + +FollowedContent + +FollowedDocumentsUrl +FollowedSitesUrl + + +yuml_basepermissions + +BasePermissions + +Low +High + + +yuml_xmlschemafieldcreationinformation + +XmlSchemaFieldCreationInformation + +Options +SchemaXml + + +yuml_listitemformupdatevalue + +ListItemFormUpdateValue + +ErrorMessage +FieldName +FieldValue +HasException + + +yuml_changelogitemquery + +ChangeLogitemQuery + +ChangeToken +Contains +Query +QueryOptions +RowLimit +ViewFields +ViewName + + +yuml_listitemcollectionposition + +ListItemCollectionPosition + +PagingInfo + + +yuml_camlquery + +CamlQuery + +DatesInUtc +FolderServerRelativeUrl +ListItemCollectionPosition +ViewXml + + +yuml_changequery + +ChangeQuery + +Add +Alert +ChangeTokenEnd +ChangeTokenStart +ContentType +DeleteObject +Field +File +Folder +Group +GroupMembershipAdd +GroupMembershipDelete +Item +List +Move +Navigation +Rename +Restore +RoleAssignmentAdd +RoleAssignmentDelete +RoleDefinitionAdd +RoleDefinitionDelete +RoleDefinitionUpdate +SecurityPolicy +Site +SystemUpdate +Update +User +View +Web + + +yuml_changetoken + +ChangeToken + +StringValue + + +yuml_subscriptionupdateresult + +SubscriptionUpdateResult + +subscription +data + + +yuml_subscriptionaddresult + +SubscriptionAddResult + +subscription +data + + +yuml_mysocialquerymethods + +MySocialQueryMethods + + + +get() +followed() +followedCount() +followers() +suggestions() +yuml_mysocialquerymethods->yuml_mysocialquery + + +yuml_socialmethods + +SocialMethods + +my + +getFollowedSitesUri() +getFollowedDocumentsUri() +follow() +isFollowed() +stopFollowing() +yuml_socialmethods->yuml_socialquery + + +yuml_mysocialdata + +MySocialData + +SocialActor +MyFollowedDocumentsUri +MyFollowedSitesUri + + +yuml_socialactor + +SocialActor + +ActorType +Id +Uri +Name +IsFollowed +Status +CanFollow +ImageUri +AccountName +EmailAddress +Title +StatusText +PersonalSiteUri +FollowedContentUri +ContentUri +LibraryUri +TagGuid + + +yuml_socialactorinfo + +SocialActorInfo + +AccountName +ActorType +ContentUri +Id +TagGuid + + +yuml_siteuserprops + +SiteUserProps + +Email +Id +IsHiddenInUI +IsShareByEmailGuestUser +IsSiteAdmin +LoginName +PrincipalType +Title + + +yuml_userupdateresult + +UserUpdateResult + +user +data + + +yuml_sitegroupaddresult + +SiteGroupAddResult + +group +data + + +yuml_groupaddresult + +GroupAddResult + +group +data + + +yuml_groupupdateresult + +GroupUpdateResult + +group +data + + +yuml_openwebbyidresult + +OpenWebByIdResult + +data +web + + +yuml_sharepointqueryableshareablefolder + +SharePointQueryableShareableFolder + + + +shareWith() +yuml_filefoldershared->yuml_sharepointqueryableshareablefolder + + +yuml_sharepointqueryableshareablefile + +SharePointQueryableShareableFile + + + +shareWith() +yuml_filefoldershared->yuml_sharepointqueryableshareablefile + + +yuml_folder + +Folder + +contentTypeOrder +files +folders +listItemAllFields +parentFolder +properties +serverRelativeUrl +uniqueContentTypeOrder + +update() +delete() +recycle() +getItem() +moveTo() +yuml_sharepointqueryableshareablefolder->yuml_folder + + +yuml_file + +File + +listItemAllFields +versions + +approve() +cancelUpload() +checkin() +checkout() +copyTo() +delete() +deny() +getLimitedWebPartManager() +moveTo() +publish() +recycle() +undoCheckout() +unpublish() +getText() +getBlob() +getBuffer() +getJSON() +setContent() +getItem() +setContentChunked() +startUpload() +continueUpload() +finishUpload() +yuml_sharepointqueryableshareablefile->yuml_file + + +yuml_sharepointqueryablesecurable->yuml_sharepointqueryableshareableweb + + +yuml_sharepointqueryableshareableitem + +SharePointQueryableShareableItem + + + +getShareLink() +shareWith() +checkSharingPermissions() +getSharingInformation() +getObjectSharingSettings() +unshare() +deleteSharingLinkByKind() +unshareLink() +yuml_sharepointqueryablesecurable->yuml_sharepointqueryableshareableitem + + +yuml_list + +List + +contentTypes +items +views +fields +forms +defaultView +userCustomActions +effectiveBasePermissions +eventReceivers +relatedFields +informationRightsManagementSettings +subscriptions +rootFolder + +getView() +update() +delete() +getChanges() +getItemsByCAMLQuery() +getListItemChangesSinceToken() +recycle() +renderListData() +renderListDataAsStream() +renderListFormData() +reserveListItemId() +getListItemEntityTypeFullName() +addValidateUpdateItemUsingPath() +yuml_sharepointqueryablesecurable->yuml_list + + +yuml_item + +Item + +attachmentFiles +contentType +comments +effectiveBasePermissions +effectiveBasePermissionsForUI +fieldValuesAsHTML +fieldValuesAsText +fieldValuesForEdit +folder +file +versions + +update() +getLikedBy() +like() +unlike() +delete() +recycle() +getWopiFrameUrl() +validateUpdateListItem() +ensureListItemEntityTypeName() +yuml_sharepointqueryableshareableitem->yuml_item + + +yuml_odataqueryable + +ODataQueryable +yuml_odataqueryable->yuml_sharepointqueryable + + +yuml_sharepointqueryableconstructor + +SharePointQueryableConstructor + + + + +yuml_personalresultsuggestion + +PersonalResultSuggestion + +HighlightedTitle +IsBestBet +Title +TypeId +Url + + +yuml_searchsuggestquery + +SearchSuggestQuery + +querytext +count +personalCount +preQuery +hitHighlighting +capitalize +culture +stemming +includePeople +queryRules +prefixMatch + + +yuml_searchsuggestresult + +SearchSuggestResult + +PeopleNames +PersonalResults +Queries + + +yuml_reorderingrule + +ReorderingRule + +MatchValue +Boost +MatchType + + +yuml_searchpropertyvalue + +SearchPropertyValue + +StrVal +BoolVal +Intval +StrArray +QueryPropertyValueTypeIndex + + +yuml_searchproperty + +SearchProperty + +Name +Value + + +yuml_sort + +Sort + +Property +Direction + + +yuml_resulttable + +ResultTable + +GroupTemplateId +ItemTemplateId +Properties +Table +Refiners +ResultTitle +ResultTitleUrl +RowCount +TableType +TotalRows +TotalRowsIncludingDuplicates + + +yuml_resulttablecollection + +ResultTableCollection + +QueryErrors +QueryId +QueryRuleId +CustomResults +RefinementResults +RelevantResults +SpecialTermResults + + +yuml_searchresponse + +SearchResponse + +ElapsedTime +Properties +PrimaryQueryResult +SecondaryQueryResults +SpellingSuggestion +TriggeredRules + + +yuml_searchresult + +SearchResult + +Rank +DocId +WorkId +Title +Author +Size +Path +Description +Write +LastModifiedTime +CollapsingStatus +HitHighlightedSummary +HitHighlightedProperties +contentclass +PictureThumbnailURL +ServerRedirectedURL +ServerRedirectedEmbedURL +ServerRedirectedPreviewURL +FileExtension +ContentTypeId +ParentLink +ViewsLifeTime +ViewsRecent +SectionNames +SectionIndexes +SiteLogo +SiteDescription +importance +SiteName +IsDocument +FileType +IsContainer +WebTemplate +SPWebUrl +UniqueId +ProgId +OriginalPath +RenderTemplateId +PartitionId +UrlZone +Culture + + +yuml_searchquery + +SearchQuery + +Querytext +QueryTemplate +EnableInterleaving +EnableStemming +TrimDuplicates +EnableNicknames +EnableFQL +EnablePhonetic +BypassResultTypes +ProcessBestBets +EnableQueryRules +EnableSorting +GenerateBlockRankLog +SourceId +RankingModelId +StartRow +RowLimit +RowsPerPage +SelectProperties +Culture +RefinementFilters +Refiners +HiddenConstraints +SortList +Timeout +HitHighlightedProperties +ClientType +PersonalizationData +ResultsUrl +QueryTag +Properties +ProcessPersonalFavorites +QueryTemplatePropertiesUrl +ReorderingRules +HitHighlightedMultivaluePropertyLimit +EnableOrderingHitHighlightedProperty +CollapseSpecification +UIlanguage +DesiredSnippetLength +MaxSnippetLength +SummaryLength + + +yuml_searchbuiltinsourceid + +SearchBuiltInSourceId + +Documents +ItemsMatchingContentType +ItemsMatchingTag +ItemsRelatedToCurrentUser +ItemsWithSameKeywordAsThisItem +LocalPeopleResults +LocalReportsAndDataResults +LocalSharePointResults +LocalVideoResults +Pages +Pictures +Popular +RecentlyChangedItems +RecommendedItems +Wiki + + +yuml_searchresults + +SearchResults + +_url +_query +_raw +_primary +ElapsedTime +RowCount +TotalRows +TotalRowsIncludingDuplicates +RawSearchResults +PrimarySearchResults + +getPage() +formatSearchResults() +yuml_searchquerybuilder + +SearchQueryBuilder + +_query +enableInterleaving +enableStemming +trimDuplicates +enableNicknames +enableFql +enablePhonetic +bypassResultTypes +processBestBets +enableQueryRules +enableSorting +generateBlockRankLog +processPersonalFavorites +enableOrderingHitHighlightedProperty + +create() +text() +template() +sourceId() +trimDuplicatesIncludeId() +rankingModelId() +startRow() +rowLimit() +rowsPerPage() +selectProperties() +culture() +timeZoneId() +refinementFilters() +refiners() +hiddenConstraints() +sortList() +timeout() +hithighlightedProperties() +clientType() +personalizationData() +resultsURL() +queryTag() +properties() +queryTemplatePropertiesUrl() +reorderingRules() +hitHighlightedMultivaluePropertyLimit() +collapseSpecification() +uiLanguage() +desiredSnippetLength() +maxSnippetLength() +summaryLength() +toSearchQuery() +extendQuery() +yuml_roledefinitionaddresult + +RoleDefinitionAddResult + +definition +data + + +yuml_roledefinitionupdateresult + +RoleDefinitionUpdateResult + +definition +data + + +yuml_sprest + +SPRest + +_options +_baseUrl +site +web +profiles +social +navigation +utility + +configure() +setup() +searchSuggest() +search() +createBatch() +create() +yuml_relateditemmanger + +RelatedItemManger + + + +getRelatedItems() +getPageOneRelatedItems() +addSingleLink() +addSingleLinkToUrl() +addSingleLinkFromUrl() +deleteSingleLink() +yuml_relateditemmanger->yuml_relateditemmanagerimpl + + +yuml_relateditem + +RelatedItem + +ListId +ItemId +Url +Title +WebId +IconUrl + + +yuml_inavigationservice + +INavigationService + + + +getMenuState() +getMenuNodeKey() +yuml_inavigationservice->yuml_navigationservice + + +yuml_navigationnodeaddresult + +NavigationNodeAddResult + +data +node + + +yuml_listensureresult + +ListEnsureResult + +list +created +data + + +yuml_listupdateresult + +ListUpdateResult + +list +data + + +yuml_listaddresult + +ListAddResult + +list +data + + +yuml_itemupdateresultdata + +ItemUpdateResultData + +odata.etag + + +yuml_itemupdateresult + +ItemUpdateResult + +item +data + + +yuml_itemaddresult + +ItemAddResult + +item +data + + +yuml_pageditemcollection + +PagedItemCollection + +parent +nextUrl +results +hasNext + +getNext() +yuml_folderupdateresult + +FolderUpdateResult + +folder +data + + +yuml_folderaddresult + +FolderAddResult + +folder +data + + +yuml_clientsidepage + +ClientSidePage + +sections +commentsDisabled + +create() +fromFile() +jsonToEscapedString() +escapedStringToJson() +addSection() +toHtml() +fromHtml() +load() +save() +enableComments() +disableComments() +findControlById() +findControl() +setCommentsOn() +mergePartToTree() +mergeColumnToTree() +updateProperties() +yuml_file->yuml_clientsidepage + + +yuml_fileaddresult + +FileAddResult + +file +data + + +yuml_chunkedfileuploadprogressdata + +ChunkedFileUploadProgressData + +uploadId +stage +blockNumber +totalBlocks +chunkSize +currentPointer +fileSize + + +yuml_fieldupdateresult + +FieldUpdateResult + +data +field + + +yuml_fieldaddresult + +FieldAddResult + +data +field + + +yuml_featureaddresult + +FeatureAddResult + +data +feature + + +yuml_error + +Error +yuml_notsupportedinbatchexception + +NotSupportedInBatchException + + + + +yuml_error->yuml_notsupportedinbatchexception + + +yuml_maxcommentlengthexception + +MaxCommentLengthException + + + + +yuml_error->yuml_maxcommentlengthexception + + +yuml_spbatchparseexception + +SPBatchParseException + + + + +yuml_error->yuml_spbatchparseexception + + +yuml_contenttypeaddresult + +ContentTypeAddResult + +contentType +data + + +yuml_commentinfo + +CommentInfo + +text +mentions + + +yuml_identity + +Identity + +loginName +email +name + + +yuml_commentdata + +CommentData + +author +createdDate +id +isLikedByUser +isReply +itemId +likeCount +listId +mentions +parentId +replyCount +text + + +yuml_commentauthordata + +CommentAuthorData + +email +id +isActive +isExternal +jobTitle +loginName +name +principalType +userId + + +yuml_clientsidepart + +ClientSidePart + + + +remove() +yuml_clientsidewebpart + +ClientSideWebpart + +title +description +propertieJson +webPartId +htmlProperties +serverProcessedContent +canvasDataVersion + +fromComponentDef() +import() +setProperties() +getProperties() +toHtml() +fromHtml() +getControlData() +renderHtmlProperties() +parseJsonProperties() +yuml_clientsidepart->yuml_clientsidewebpart + + +yuml_clientsidetext + +ClientSideText + +_text +text + +getControlData() +toHtml() +fromHtml() +yuml_clientsidepart->yuml_clientsidetext + + +yuml_canvascontrol + +CanvasControl + +controlType +dataVersion +column +order +id +controlData +jsonData + +toHtml() +fromHtml() +getControlData() +yuml_canvascontrol->yuml_clientsidepart + + +yuml_canvascolumn + +CanvasColumn + +section +order +factor +controls + +addControl() +getControl() +toHtml() +fromHtml() +getControlData() +remove() +yuml_canvascontrol->yuml_canvascolumn + + +yuml_clientsidewebpartdata + +ClientSideWebpartData + +dataVersion +description +id +instanceId +properties +title +serverProcessedContent + + +yuml_clientsidecontroldata + +ClientSideControlData + +controlType +id +editorType +position +webPartId +displayMode + + +yuml_clientsidecontrolposition + +ClientSideControlPosition + +controlIndex +sectionFactor +sectionIndex +zoneIndex + + +yuml_serverprocessedcontent + +ServerProcessedContent + +searchablePlainTexts +imageSources +links + + +yuml_clientsidepagecomponent + +ClientSidePageComponent + +ComponentType +Id +Manifest +ManifestType +Name +Status + + +yuml_canvassection + +CanvasSection + +page +order +columns +_memId +defaultColumn + +addColumn() +addControl() +toHtml() +remove() +yuml_odatabatch + +ODataBatch +yuml_spbatch + +SPBatch + +baseUrl + +ParseResponse() +executeImpl() +yuml_odatabatch->yuml_spbatch + + +yuml_attachmentfileaddresult + +AttachmentFileAddResult + +file +data + + +yuml_attachmentfileinfo + +AttachmentFileInfo + +name +content + + +yuml_appaddresult + +AppAddResult + +data +file + + + + diff --git a/v1/documentation/package-structure/index.html b/v1/documentation/package-structure/index.html new file mode 100644 index 000000000..dd0eff086 --- /dev/null +++ b/v1/documentation/package-structure/index.html @@ -0,0 +1,1875 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Package Structure - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Package Structure

+

Each of the packages is published with the same structure, so this article applies to all of the packages. We will use @pnp/core as an example for discussion.

+

Folders

+

In addition to the files in the root each package has three folders dist, docs, and src.

+

Root Files

+

These files are found at the root of each package.

+ + + + + + + + + + + + + + + + + + + + + + + + + +
FileDescription
index.d.tsReferenced in package.json typings property and provides the TypeScript type information for consumers
LICENSEPackage license
package.jsonnpm package definition
readme.mdBasic readme referencing the docs site
+

Dist

+

The dist folder contains the transpiled files bundled in various ways. You can choose the best file for your usage as needed. Below the {package} will be +replaced with the name of the package - in our examples case this would be "common" making the file name "{package}.es5.js" = "common.es5.js". All of the *.map +files are the debug mapping files related to the .js file of the same name.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileDescription
{package}.es5.jsLibrary packaged in es5 format not wrapped as a module
{package}.es5.umd.bundle.jsThe library bundled with all dependencies into a single UMD module. Global variable will be "pnp.{package}". Referenced in the main property of package.json
{package}.es5.umd.bundle.min.jsMinified version of the bundled umd module
{package}.es5.umd.jsThe library in es5 bundled as a UMD modules with no included dependencies. They are designed to work with the other *.es5.umd.js files. Referenced in the module property of package.json
{package}.es5.umd.min.jsMinified version of the es5 umd module
{package}.jses6 format file of the library. Referenced by es2015 property of package.json
+

Docs

+

This folder contains markdown documentation for the library. All packages will include an index.md which serves as the root of the docs. These files are also used +to build the public site. To edit these files they can be found in the packages/{package}/docs folder.

+

Src

+

Contains the TypeScript definition files refrenced by the index.d.ts in the package root. These files serve to provide typing information about the library to +consumers who can process typing information.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/packages/index.html b/v1/documentation/packages/index.html new file mode 100644 index 000000000..c7e8d6a0d --- /dev/null +++ b/v1/documentation/packages/index.html @@ -0,0 +1,1741 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Packages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + +
+
+ + + + + +

Packages

+ +

The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within +the @pnp scope.

+

The latest published version is ****.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@pnp/
commonProvides shared functionality across all pnp libraries
config-storeProvides a way to manage configuration within your application
graphProvides a fluent api for working with Microsoft Graph
loggingLight-weight, subscribable logging framework
nodejsProvides functionality enabling the @pnp libraries within nodejs
odataProvides shared odata functionality and base classes
pnpjsRollup library of core functionality (mimics sp-pnp-js)
spProvides a fluent api for working with SharePoint REST
sp-addinhelpersProvides functionality for working within SharePoint add-ins
sp-clientsvcProvides base classes for working with the legacy SharePoint
sp-taxonomyProvides a fluent api for working with SharePoint Managed Metadata
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/polyfill/index.html b/v1/documentation/polyfill/index.html new file mode 100644 index 000000000..d3cf37eec --- /dev/null +++ b/v1/documentation/polyfill/index.html @@ -0,0 +1,1877 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Polyfills - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Polyfills

+

These libraries may make use of some features not found in older browsers, mainly fetch, Map, and Proxy. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. There are several ways to include this missing functionality.

+

IE 11 Polyfill package

+

We created a package you can use to include the needed functionality without having to determine what polyfills are required. Also, this package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you need to support IE 11.

+

Install

+

npm install --save @pnp/polyfill-ie11

+

Use

+
import "@pnp/polyfill-ie11";
+import { sp } from "@pnp/sp";
+
+sp.web.lists.getByTitle("BigList").items.filter(`ID gt 6000`).get().then(r => {
+  this.domElement.innerHTML += r.map(l => `${l.Title}<br />`);
+});
+
+ + +

SearchQueryBuilder

+

Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version for IE 11 as shown below.

+
import "@pnp/polyfill-ie11";
+import { SearchQueryBuilder } from "@pnp/polyfill-ie11/dist/searchquerybuilder";
+import { sp, ISearchQueryBuilder } from "@pnp/sp";
+
+// works in IE11 and other browsers
+const builder: ISearchQueryBuilder = SearchQueryBuilder().text("test");
+
+sp.search(builder).then(r => {
+  this.domElement.innerHTML = JSON.stringify(r);
+});
+
+ + +

Polyfill Service

+

If acceptable to your design and security requirements you can use a service to provide missing functionality. This loads scripts from a service outside of your and our +control, so please ensure you understand any associated risks.

+

To use this option you need to wrap the code in a function, here called "stuffisloaded". Then you need to add another script tag as shown below that will load what you need from the polyfill service. Note the parameter "callback" takes our function name.

+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.2.1/pnpjs.es5.umd.bundle.min.js" type="text/javascript"></script>
+<script>
+// this function will be executed once the polyfill is loaded.
+function stuffisloaded() {
+
+  pnp.sp.web.select("Title").get()
+    .then(function(data){
+      document.getElementById("main").innerText=data.Title;
+  })   
+  .catch(function(err){  
+    document.getElementById("main").innerText=err;
+  });
+}
+</script>
+<!-- This script tag loads the required polyfills from the service -->
+<script src="https://cdn.polyfill.io/v2/polyfill.min.js?callback=stuffisloaded&features=es6,fetch,Map&flags=always,gated"></script>
+
+ + +

Module Loader

+

If you are using a module loader you need to load the following two files as well. You can do this form a CDN or your style library.

+
    +
  1. Download the es6-promises polyfill from https://github.com/stefanpenner/es6-promise and upload it to your style library.
  2. +
  3. Download the fetch polyfill from https://github.com/github/fetch and upload it to your style library.
  4. +
  5. Download the corejs polyfill from https://github.com/zloirock/core-js and upload it to your style library.
  6. +
  7. Update your module loader to set these files as dependencies before the pnp library is opened.
  8. +
+

One issue you still may see is that you get errors that certain libraries are undefined when you try to run your code. This is because your code is running before +these libraries are loaded. You need to ensure that all dependencies are loaded before making use of the pnp libraries.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/documentation/theme/main.html b/v1/documentation/theme/main.html new file mode 100644 index 000000000..2f621a908 --- /dev/null +++ b/v1/documentation/theme/main.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block analytics %} + spacer +{% endblock %} diff --git a/v1/documentation/transition-guide/index.html b/v1/documentation/transition-guide/index.html new file mode 100644 index 000000000..16bb9f481 --- /dev/null +++ b/v1/documentation/transition-guide/index.html @@ -0,0 +1,1977 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Transition Guide - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Transition Guide

+

These libraries are based on the sp-pnp-js library and our goal was to make transition as easy as possible. The most +obvious difference is the splitting of the library into multiple packages. We have however created a rollup library to help folks make the move - though our +recommendation is to switch to the separate packages. This article outlines transitioning your existing projects from sp-pnp-js to the new libraries, please provide +feedback on how we can improve out guidance.

+

Installing @pnp libraries

+

With the separation of the packages we needed a way to indicate how they are related, while making things easy for folks to track and update and we have used peer +dependencies between the packages to do this. With each release we will release all packages so that the version numbers move in lock-step, making it easy to ensure +you are working with compatible versions. One thing to keep in mind with peer dependencies is that they are not automatically installed. The advantage is you +will only have one copy of each library in your project.

+

Installing peer dependencies is easy, you can specify each of the packages in a single line, here we are installing everything required to use the @pnp/sp package.

+
npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp
+
+ + +

If you do not install all of the peer dependencies you will get a message specifying which ones are missing along with the version expected.

+

Import Simplification

+

With the separation of packages we have also simplified the imports, and allowed you more control over what you are importing. Compare these two examples showing +the same set of imports, but one is done via sp-pnp-js and the other using the @pnp libraries.

+

From sp-pnp-js

+
import pnp, {
+  Web,
+  Util,
+  Logger,
+  FunctionListener,
+  LogLevel,
+} from "sp-pnp-js";
+
+ + +

From @pnp libraries

+
import {
+  Logger,
+  LogLevel,
+  FunctionListener
+} from "@pnp/logging";
+
+import * as Util from "@pnp/core";
+
+import {
+  sp,
+  Web
+} from "@pnp/sp";
+
+ + +

In the above example the "sp" import replaces "pnp" and is the root of your method chains. Once we have updated our imports we have a few small code changes to make, +depending on how you have used the library in your applications. Watch this short video discussing the most common updates:

+ + +

Updated settings file format

+

If you are doing local debugging or testing you have likely created a settings.js from the supplied settings.example.js. Please note the format of that file has changed, +the new format is shown below.

+
var settings = {
+
+    spsave: {
+        username: "develina.devsson@mydevtenant.onmicrosoft.com",
+        password: "pass@word1",
+        siteUrl: "https://mydevtenant.sharepoint.com/"
+    },
+    testing: {
+        enableWebTests: true,
+        sp: {
+            id: "{ client id }",
+            secret: "{ client secret }",
+            url: "{ site collection url }",
+            notificationUrl: "{ notification url }",
+        },
+        graph: {
+            tenant: "{tenant.onmicrosoft.com}",
+            id: "{your app id}",
+            secret: "{your secret}"
+        },
+    }
+}
+
+ + +

HttpClient Renamed

+

If you used HttpClient from sp-pnp-js it was renamed to SPHttpClient. A transition to @pnp/sp assumes replacement of:

+
import { HttpClient } from 'sp-pnp-js';
+
+ + +

to the following import statement:

+
import { SPHttpClient } from '@pnp/sp';
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/contacts/index.html b/v1/graph/docs/contacts/index.html new file mode 100644 index 000000000..fbe018355 --- /dev/null +++ b/v1/graph/docs/contacts/index.html @@ -0,0 +1,2080 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + contacts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/graph/contacts

+

The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described +you can add and edit both contacts and folders in a users Outlook.

+

Get all of the Contacts

+

Using the contacts() you can get the users contacts from Outlook

+
import { graph } from "@pnp/graph";
+
+const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.get();
+
+const contacts = await graph.me.contacts.get();
+
+ + +

Add a new Contact

+

Using the contacts.add() you can a add Contact to the users Outlook

+
import { graph } from "@pnp/graph";
+
+const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+const addedContact = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+ + +

Get Contact by Id

+

Using the contacts.getById() you can get one of the users Contacts in Outlook

+
import { graph } from "@pnp/graph";
+
+const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId');
+
+const contact = await graph.me.contacts.getById('userId');
+
+ + +

Delete a Contact

+

Using the delete you can remove one of the users Contacts in Outlook

+
import { graph } from "@pnp/graph";
+
+const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId').delete();
+
+const delContact = await graph.me.contacts.getById('userId').delete();
+
+ + +

Update a Contact

+

Using the update you can update one of the users Contacts in Outlook

+
import { graph } from "@pnp/graph";
+
+const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById('userId').update({birthday: "1986-05-30" });
+
+const updContact = await graph.me.contacts.getById('userId').update({birthday: "1986-05-30" });
+
+ + +

Get all of the Contact Folders

+

Using the contactFolders() you can get the users Contact Folders from Outlook

+
import { graph } from "@pnp/graph";
+
+const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.get();
+
+const contactFolders = await graph.me.contactFolders.get();
+
+ + +

Add a new Contact Folder

+

Using the contactFolders.add() you can a add Contact Folder to the users Outlook

+
import { graph } from "@pnp/graph";
+
+const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add('displayName', '<ParentFolderId>');
+
+const addedContactFolder = await graph.me.contactFolders.contactFolders.add('displayName', '<ParentFolderId>');
+
+ + +

Get Contact Folder by Id

+

Using the contactFolders.getById() you can get one of the users Contact Folders in Outlook

+
import { graph } from "@pnp/graph";
+
+const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId');
+
+const contactFolder = await graph.me.contactFolders.getById('folderId');
+
+ + +

Delete a Contact Folder

+

Using the delete you can remove one of the users Contact Folders in Outlook

+
import { graph } from "@pnp/graph";
+
+const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId').delete();
+
+const delContactFolder = await graph.me.contactFolders.getById('folderId').delete();
+
+ + +

Update a Contact Folder

+

Using the update you can update one of the users Contact Folders in Outlook

+
import { graph } from "@pnp/graph";
+
+const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('userId').update({displayName: "value" });
+
+const updContactFolder = await graph.me.contactFolders.getById('userId').update({displayName: "value" });
+
+ + +

Get all of the Contacts from the Contact Folder

+

Using the contacts() in the Contact Folder gets the users Contact from the folder.

+
import { graph } from "@pnp/graph";
+
+const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('folderId').contacts.get();
+
+const contactsInContactFolder = await graph.me.contactFolders.getById('folderId').contacts.get();
+
+ + +

Get Child Folders of the Contact Folder

+

Using the childFolders() you can get the Child Folders of the current Contact Folder from Outlook

+
import { graph } from "@pnp/graph";
+
+const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.get();
+
+const childFolders = await graph.me.contactFolders.getById('<id>').childFolders.get();
+
+ + +

Add a new Child Folder

+

Using the childFolders.add() you can a add Child Folder in a Contact Folder

+
import { graph } from "@pnp/graph";
+
+const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.add('displayName', '<ParentFolderId>');
+
+const addedChildFolder = await graph.me.contactFolders.getById('<id>').childFolders.add('displayName', '<ParentFolderId>');
+
+ + +

Get Child Folder by Id

+

Using the childFolders.getById() you can get one of the users Child Folders in Outlook

+
import { graph } from "@pnp/graph";
+
+const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.getById('folderId');
+
+const childFolder = await graph.me.contactFolders.getById('<id>').childFolders.getById('folderId');
+
+ + +

Add Contact in Child Folder of Contact Folder

+

Using contacts.add in the Child Folder of a Contact Folder, adds a new Contact to that folder

+
import { graph } from "@pnp/graph";
+
+const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById('<id>').childFolders.getById('folderId').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+const addedContact = await graph.me.contactFolders.getById('<id>').childFolders.getById('folderId').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/directoryobjects/index.html b/v1/graph/docs/directoryobjects/index.html new file mode 100644 index 000000000..2f1fc4f1b --- /dev/null +++ b/v1/graph/docs/directoryobjects/index.html @@ -0,0 +1,1865 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + directory objects - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/graph/directoryObjects

+

The groups and directory roles for the user

+
import { graph } from "@pnp/graph";
+
+const memberOf = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').memberOf.get();
+
+const memberOf = await graph.me.memberOf.get();
+
+ + +

Return all the groups the user, group or directoryObject is a member of

+
import { graph } from "@pnp/graph";
+
+const memberGroups = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups();
+
+const memberGroups = await graph.me.getMemberGroups();
+
+const memberGroups = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups();
+
+const memberGroups = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberGroups();
+
+ + +

Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of.

+
import { graph } from "@pnp/graph";
+
+const memberObjects = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();
+
+const memberObjects = await graph.me.getMemberObjects();
+
+const memberObjects = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();
+
+const memberObjects = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();
+
+ + +

Check for membership in a specified list of groups

+

And returns from that list those groups of which the specified user, group, or directory object is a member

+
import { graph } from "@pnp/graph";
+
+const checkedMembers = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers = await graph.me.checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+const checkedMembers = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
+
+ + +

Get directoryObject by Id

+
import { graph } from "@pnp/graph";
+
+const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').get();
+
+ + +

Delete directoryObject

+
import { graph } from "@pnp/graph";
+
+const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/index.html b/v1/graph/docs/index.html new file mode 100644 index 000000000..d3a4e2b65 --- /dev/null +++ b/v1/graph/docs/index.html @@ -0,0 +1,1858 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + graph - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/graph

+

npm version

+

This package contains the fluent api used to call the graph rest services.

+

Getting Started

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save

+

Import the library into your application and access the root sp object

+
import { graph } from "@pnp/graph";
+
+(function main() {
+
+    // here we will load the current web's properties
+    graph.groups.get().then(g => {
+
+        console.log(`Groups: ${JSON.stringify(g, null, 4)}`);
+    });
+})()
+
+ + +

Getting Started with SharePoint Framework

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save

+

Import the library into your application, update OnInit, and access the root sp object in render

+
import { graph } from "@pnp/graph";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+
+    graph.setup({
+      spfxContext: this.context
+    });
+  });
+}
+
+// ...
+
+public render(): void {
+
+    // A simple loading message
+    this.domElement.innerHTML = `Loading...`;
+
+    // here we will load the current web's properties
+    graph.groups.get().then(groups => {
+
+        this.domElement.innerHTML = `Groups: <ul>${groups.map(g => `<li>${g.displayName}</li>`).join("")}</ul>`;
+    });
+}
+
+ + +

Getting Started on Nodejs

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save

+

Import the library into your application, setup the node client, make a request

+
import { graph } from "@pnp/graph";
+import { AdalFetchClient } from "@pnp/nodejs";
+
+// do this once per page load
+graph.setup({
+    graph: {
+        fetchClientFactory: () => {
+            return new AdalFetchClient("{tenant}.onmicrosoft.com", "AAD Application Id", "AAD Application Secret");
+        },
+    },
+});
+
+// here we will load the groups information
+graph.groups.get().then(g => {
+
+    console.log(`Groups: ${JSON.stringify(g, null, 4)}`);
+});
+
+ + +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/graph. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/insights/index.html b/v1/graph/docs/insights/index.html new file mode 100644 index 000000000..56bab1d8f --- /dev/null +++ b/v1/graph/docs/insights/index.html @@ -0,0 +1,1698 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @pnp/graph/insights - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/graph/insights

+

Insights are relationships calculated using advanced analytics and machine learning techniques. You can, for example, identify OneDrive documents trending around users.

+ +

Using the trending() returns documents from OneDrive and from SharePoint sites trending around a user.

+
import { graph } from "@pnp/graph";
+
+const trending = await graph.users.getById('user@tenant.onmicrosoft.com').insights.trending.get();
+
+const trending = await graph.me.insights.trending.get();
+
+ + +

Get the used documents

+

Using the used() returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.

+
import { graph } from "@pnp/graph";
+
+const used = await graph.users.getById('user@tenant.onmicrosoft.com').insights.used.get();
+
+const used = await graph.me.insights.used.get();
+
+ + +

Get the shared documents

+

Using the shared() returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.

+
import { graph } from "@pnp/graph";
+
+const shared = await graph.users.getById('user@tenant.onmicrosoft.com').insights.shared.get();
+
+const shared = await graph.me.insights.shared.get();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/invitations/index.html b/v1/graph/docs/invitations/index.html new file mode 100644 index 000000000..74d50db4c --- /dev/null +++ b/v1/graph/docs/invitations/index.html @@ -0,0 +1,1742 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + invitations - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/graph/invitations

+

The ability invite an external user via the invitation manager

+

Create Invitation

+

Using the invitations.create() you can create an Invitation. +We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).

+
import { graph } from "@pnp/graph";
+
+const invitationResult = await graph.invitations.create('external.user@emailadress.com', 'https://tenant.sharepoint.com/sites/redirecturi');
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/onedrive/index.html b/v1/graph/docs/onedrive/index.html new file mode 100644 index 000000000..c52e8090e --- /dev/null +++ b/v1/graph/docs/onedrive/index.html @@ -0,0 +1,2086 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + onedrive - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/graph/onedrive

+

The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can manage drives and drive items in Onedrive.

+

Get the default drive

+

Using the drive() you can get the default drive from Onedrive

+
import { graph } from "@pnp/graph";
+
+const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives.get();
+
+const drives = await graph.me.drives.get();
+
+ + +

Get all of the drives

+

Using the drives() you can get the users available drives from Onedrive

+
import { graph } from "@pnp/graph";
+
+const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives.get();
+
+const drives = await graph.me.drives.get();
+
+ + +

Get drive by Id

+

Using the drives.getById() you can get one of the available drives in Outlook

+
import { graph } from "@pnp/graph";
+
+const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId');
+
+const drive = await graph.me.drives.getById('driveId');
+
+ + +

Get the associated list of a drive

+

Using the list() you get the associated list

+
import { graph } from "@pnp/graph";
+
+const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list.get();
+
+const list = await graph.me.drives.getById('driveId').list.get();
+
+ + +

Get the recent files

+

Using the recent() you get the recent files

+
import { graph } from "@pnp/graph";
+
+const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent.get();
+
+const files = await graph.me.drives.getById('driveId').recent.get();
+
+ + +

Get the files shared with me

+

Using the sharedWithMe() you get the files shared with the user

+
import { graph } from "@pnp/graph";
+
+const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe.get();
+
+const shared = await graph.me.drives.getById('driveId').sharedWithMe.get();
+
+ + +

Get the Root folder

+

Using the root() you get the root folder

+
import { graph } from "@pnp/graph";
+
+const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.get();
+
+const root = await graph.me.drives.getById('driveId').root.get();
+
+ + +

Get the Children

+

Using the children() you get the children

+
import { graph } from "@pnp/graph";
+
+const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.get();
+
+const rootChildren = await graph.me.drives.getById('driveId').root.children.get();
+
+const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children.get();
+
+const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children.get();
+
+ + +

Add folder or item

+

Using the add you can add a folder or an item

+
import { graph } from "@pnp/graph";
+import { DriveItem as IDriveItem } from "@microsoft/microsoft-graph-types";
+
+const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', <IDriveItem>{folder: {}});
+
+const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', <IDriveItem>{folder: {}});
+
+ + +

Search items

+

Using the search() you can search for items, and optionally select properties

+
import { graph } from "@pnp/graph";
+
+const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText').get();
+
+const search = await graph.me.drives.getById('driveId')root.search('queryText').get();
+
+ + +

Get specific item in drive

+

Using the items.getById() you can get a specific item from the current drive

+
import { graph } from "@pnp/graph";
+
+const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId');
+
+const item = await graph.me.drives.getById('driveId').items.getById('itemId');
+
+ + +

Get thumbnails

+

Using the thumbnails() you get the thumbnails

+
import { graph } from "@pnp/graph";
+
+const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails.get();
+
+const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails.get();
+
+ + +

Delete drive item

+

Using the delete() you delete the current item

+
import { graph } from "@pnp/graph";
+
+const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete();
+
+const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete();
+
+ + +

Update drive item

+

Using the update() you update the current item

+
import { graph } from "@pnp/graph";
+
+const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: "New Name"});
+
+const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: "New Name"});
+
+ + +

Move drive item

+

Using the move() you move the current item, and optionally update it

+
import { graph } from "@pnp/graph";
+
+// Requires a parentReference to the new folder location
+const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"});
+
+const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/people/index.html b/v1/graph/docs/people/index.html new file mode 100644 index 000000000..5587fbdd1 --- /dev/null +++ b/v1/graph/docs/people/index.html @@ -0,0 +1,1664 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @pnp/graph/people - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/graph/people

+

The ability to retrieve a list of person objects ordered by their relevance to the user, which is determined by the user's communication and collaboration patterns, and business relationships.

+

Get all of the people

+

Using the people() you can retrieve a list of person objects ordered by their relevance to the user.

+
import { graph } from "@pnp/graph";
+
+const people = await graph.users.getById('user@tenant.onmicrosoft.com').people.get();
+
+const people = await graph.me.people.get();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/planner/index.html b/v1/graph/docs/planner/index.html new file mode 100644 index 000000000..ea2d74a80 --- /dev/null +++ b/v1/graph/docs/planner/index.html @@ -0,0 +1,2097 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + planner - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/graph/planner

+

The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can add, update and delete items in Planner.

+

Get Plans by Id

+

Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan.

+
import { graph } from "@pnp/graph";
+
+const plan = await graph.planner.plans.getById('planId');
+
+ + +

Add new Plan

+

Using the planner.plans.add() you can create a new Plan.

+
import { graph } from "@pnp/graph";
+
+const newPlan = await graph.planner.plans.add('groupObjectId', 'title');
+
+ + +

Get Tasks in Plan

+

Using the tasks() you can get the Tasks in a Plan.

+
import { graph } from "@pnp/graph";
+
+const planTasks = await graph.planner.plans.getById('planId').tasks.get();
+
+ + +

Get Buckets in Plan

+

Using the buckets() you can get the Buckets in a Plan.

+
import { graph } from "@pnp/graph";
+
+const planBuckets = await graph.planner.plans.getById('planId').buckets.get();
+
+ + +

Get Details in Plan

+

Using the details() you can get the details in a Plan.

+
import { graph } from "@pnp/graph";
+
+const planDetails = await graph.planner.plans.getById('planId').details.get();
+
+ + +

Delete Plan

+

Using the delete() you can get delete a Plan.

+
import { graph } from "@pnp/graph";
+
+const delPlan = await graph.planner.plans.getById('planId').delete();
+
+ + +

Update Plan

+

Using the update() you can get update a Plan.

+
import { graph } from "@pnp/graph";
+
+const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title'});
+
+ + +

Get Task by Id

+

Using the planner.tasks.getById() you can get a specific Task. +Planner.tasks is not an available endpoint, you need to get a specific Task.

+
import { graph } from "@pnp/graph";
+
+const task = await graph.planner.tasks.getById('taskId');
+
+ + +

Add new Task

+

Using the planner.tasks.add() you can create a new Task.

+
import { graph } from "@pnp/graph";
+
+const newTask = await graph.planner.tasks.add('planId', 'title');
+
+ + +

Get Details in Task

+

Using the details() you can get the details in a Task.

+
import { graph } from "@pnp/graph";
+
+const taskDetails = await graph.planner.tasks.getById('taskId').details.get();
+
+ + +

Delete Task

+

Using the delete() you can get delete a Task.

+
import { graph } from "@pnp/graph";
+
+const delTask = await graph.planner.tasks.getById('taskId').delete();
+
+ + +

Update Task

+

Using the update() you can get update a Task.

+
import { graph } from "@pnp/graph";
+
+const updTask = await graph.planner.tasks.getById('taskId').update({properties});
+
+ + +

Get Buckets by Id

+

Using the planner.buckets.getById() you can get a specific Bucket. +planner.buckets is not an available endpoint, you need to get a specific Bucket.

+
import { graph } from "@pnp/graph";
+
+const bucket = await graph.planner.buckets.getById('bucketId');
+
+ + +

Add new Bucket

+

Using the planner.buckets.add() you can create a new Bucket.

+
import { graph } from "@pnp/graph";
+
+const newBucket = await graph.planner.buckets.add('name', 'planId');
+
+ + +

Update Bucket

+

Using the update() you can get update a Bucket.

+
import { graph } from "@pnp/graph";
+
+const updBucket = await graph.planner.buckets.getById('bucketId').update({name: "Name"});
+
+ + +

Delete Bucket

+

Using the delete() you can get delete a Bucket.

+
import { graph } from "@pnp/graph";
+
+const delBucket = await graph.planner.buckets.getById('bucketId').delete();
+
+ + +

Get Bucket Tasks

+

Using the tasks() you can get Tasks in a Bucket.

+
import { graph } from "@pnp/graph";
+
+const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks.get();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/security/index.html b/v1/graph/docs/security/index.html new file mode 100644 index 000000000..5ead1658c --- /dev/null +++ b/v1/graph/docs/security/index.html @@ -0,0 +1,1692 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @pnp/graph/security - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/graph/security

+

The Microsoft Graph Security API can be used as a federated security aggregation service to submit queries to all onboarded security providers to get aggregated responses.

+

Get all Alerts

+

Using the alerts() to retrieve a list of Alert objects

+
import { graph } from "@pnp/graph";
+
+const alerts = await graph.security.alerts.get();
+
+ + +

Get an Alert by Id

+

Using the alerts.getById() to retrieve a specific Alert object

+
import { graph } from "@pnp/graph";
+
+const alert = await graph.security.alerts.getById('alertId').get();
+
+ + +

Update an Alert

+

Using the alerts.getById().update() to retrieve a specific Alert object

+
import { graph } from "@pnp/graph";
+
+const updAlert = await graph.security.alerts.getById('alertId').update({status: 'Status' });
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/sites/index.html b/v1/graph/docs/sites/index.html new file mode 100644 index 000000000..e89c9cf52 --- /dev/null +++ b/v1/graph/docs/sites/index.html @@ -0,0 +1,2043 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @pnp/graph/sites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/graph/sites

+

The ability to manage sites, lists and listitems in SharePoint is a capability introduced in version 1.3.0 of @pnp/graph.

+

Get the Root Site

+

Using the sites.root()() you can get the tenant root site

+
import { graph } from "@pnp/graph";
+
+const tenantRootSite = await graph.sites.root.get()
+
+ + +

Get the Root Site by Id

+

Using the sites.getById()() you can get the root site as well

+
import { graph } from "@pnp/graph";
+
+const tenantRootSite = await graph.sites.getById('contoso.sharepoint.com').get()
+
+ + +

Access a Site by server-relative URL

+

Using the sites.getById()() you can get a specific site. With the combination of the base URL and a relative URL. +We are using an internal method for combining the URL in the right combination, with : ex: contoso.sharepoint.com:/sites/site1:

+

Here are a few url combinations that works:

+
import { graph } from "@pnp/graph";
+
+// No / in the URLs
+const siteByRelativeUrl = await graph.sites.getById('contoso.sharepoint.com', 'sites/site1').get()
+
+// Both trailing / in the base URL and starting / in the relative URL
+const siteByRelativeUrl = await graph.sites.getById('contoso.sharepoint.com/', '/sites/site1').get()
+
+// Both trailing / in the base URL and starting and trailing / in the relative URL
+const siteByRelativeUrl = await graph.sites.getById('contoso.sharepoint.com/', '/sites/site1/').get()
+
+ + +

Get the Sub Sites in a Site

+

Using the sites()() you can get the sub sites of a site. As this is returned as Sites, you could use getById() for a specific site and use the operations.

+
import { graph } from "@pnp/graph";
+
+const subsites = await graph.sites.getById('contoso.sharepoint.com').sites.get();
+
+ + +
+

Get Content Types

+

Using the contentTypes()() you can get the Content Types from a Site or from a List

+
import { graph } from "@pnp/graph";
+
+const contentTypesFromSite = await graph.sites.getById('contoso.sharepoint.com').contentTypes.get();
+
+const contentTypesFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').contentTypes.get();
+
+ + +

Get Specific Content Type

+

Using the getById() you can get a specific Content Type from a Site or from a List

+
import { graph } from "@pnp/graph";
+
+const contentTypeFromSite = await graph.sites.getById('contoso.sharepoint.com').contentTypes.getById('contentTypeId').get();
+
+const contentTypeFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').contentTypes.getById('contentTypeId').get();
+
+ + +
+

Get the Lists in a Site

+

Using the lists() you can get the lists of a site.

+
import { graph } from "@pnp/graph";
+
+const lists = await graph.sites.getById('contoso.sharepoint.com').lists.get();
+
+ + +

Get a specific List in a Site

+

Using the lists.getById() you can get the lists of a site.

+
import { graph } from "@pnp/graph";
+
+const list = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').get();
+
+ + +

Create a Lists in a Site

+

Using the lists.create() you can create a list in a site.

+
import { graph } from "@pnp/graph";
+
+const newLists = await graph.sites.getById('contoso.sharepoint.com').lists.create('DisplayName', {contentTypesEnabled: true, hidden: false, template: "genericList"})
+
+ + +
+

Get the default drive

+

Using the drive() you can get the default drive from a Site or a List

+
import { graph } from "@pnp/graph";
+
+const drive = await graph.sites.getById('contoso.sharepoint.com').drive.get();
+
+const drive = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').drive.get();
+
+ + +

Get all of the drives

+

Using the drives() you can get the drives from the Site

+
import { graph } from "@pnp/graph";
+
+const drives = await graph.sites.getById('contoso.sharepoint.com').drives.get();
+
+ + +

Get drive by Id

+

Using the drives.getById() you can get one specific Drive. For more operations make sure to have a look in the onedrive documentation.

+
import { graph } from "@pnp/graph";
+
+const drive = await raph.sites.getById('contoso.sharepoint.com').lists.getById('listId').drives.getById('driveId').get();
+
+ + +
+

Get Columns

+

Using the columns() you can get the columns from a Site or from a List

+
import { graph } from "@pnp/graph";
+
+const columnsFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.get();
+
+const columnsFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.get();
+
+ + +

Get Specific Column

+

Using the columns.getById() you can get a specific column from a Site or from a List

+
import { graph } from "@pnp/graph";
+
+const columnFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.getById('columnId').get();
+
+const columnsFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.getById('columnId').get();
+
+ + + +

Using the column.columnLinks() you can get the column links for a specific column, from a Site or from a List

+
import { graph } from "@pnp/graph";
+
+const columnLinksFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.getById('columnId').columnLinks.get();
+
+const columnLinksFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.getById('columnId').columnLinks.get();
+
+ + + +

Using the column.columnLinks().getById() you can get a specific column link for a specific column, from a Site or from a List

+
import { graph } from "@pnp/graph";
+
+const columnLinkFromSite = await graph.sites.getById('contoso.sharepoint.com').columns.getById('columnId').columnLinks.getById('columnLinkId').get();
+
+const columnLinkFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').columns.getById('columnId').columnLinks.getById('columnLinkId').get();
+
+ + +
+

Get Items

+

Using the items() you can get the Items from a List

+
import { graph } from "@pnp/graph";
+
+const itemsFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.get();
+
+ + +

Get Specific Item

+

Using the getById()() you can get a specific Item from a List

+
import { graph } from "@pnp/graph";
+
+const itemFromList = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').get();
+
+ + +

Create Item

+

Using the items.create() you can create an Item in a List.

+
import { graph } from "@pnp/graph";
+
+const newItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.create({
+    "Title": "Widget",
+    "Color": "Purple",
+    "Weight": 32
+});
+
+ + +

Update Item

+

Using the update() you can update an Item in a List.

+
import { graph } from "@pnp/graph";
+
+const Item = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').update({
+{
+    "Color": "Fuchsia"
+}
+})
+
+ + +

Delete Item

+

Using the delete() you can delete an Item in a List.

+
import { graph } from "@pnp/graph";
+
+const Item = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').delete()
+
+ + +

Get Fields from Item

+

Using the fields() you can the Fields in an Item

+
import { graph } from "@pnp/graph";
+
+const fieldsFromItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').fields.get();
+
+ + +

Get Versions from Item

+

Using the versions() you can the Versions of an Item

+
import { graph } from "@pnp/graph";
+
+const versionsFromItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').versions.get();
+
+ + +

Get Version from Item

+

Using the versions.getById()() you can the Versions of an Item

+
import { graph } from "@pnp/graph";
+
+const versionFromItem = await graph.sites.getById('contoso.sharepoint.com').lists.getById('listId').items.getById('itemId').versions.getById('versionId').get();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/subscriptions/index.html b/v1/graph/docs/subscriptions/index.html new file mode 100644 index 000000000..effe96e07 --- /dev/null +++ b/v1/graph/docs/subscriptions/index.html @@ -0,0 +1,1835 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + subscriptions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/graph/subscriptions

+

The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: + Mail, events, and contacts from Outlook. + Conversations from Office Groups. + Drive root items from OneDrive. + Users and Groups from Azure Active Directory. +* Alerts from the Microsoft Graph Security API.

+

Get all of the Subscriptions

+

Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.

+
import { graph } from "@pnp/graph";
+
+const subscriptions = await graph.subscriptions.get();
+
+ + +

Create a new Subscription

+

Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. +To learn more about the scopes visit this url.

+
import { graph } from "@pnp/graph";
+
+const addedSubscription = await graph.subscriptions.add("created,updated", "https://webhook.azurewebsites.net/api/send/myNotifyClient", "me/mailFolders('Inbox')/messages", "2019-11-20T18:23:45.9356913Z");
+
+ + +

Get Subscription by Id

+

Using the subscriptions.getById() you can get one of the subscriptions

+
import { graph } from "@pnp/graph";
+
+const subscription = await graph.subscriptions.getById('subscriptionId');
+
+ + +

Delete a Subscription

+

Using the subscriptions.getById().delete() you can remove one of the Subscriptions

+
import { graph } from "@pnp/graph";
+
+const delSubscription = await graph.subscription.getById('subscriptionId').delete();
+
+ + +

Update a Subscription

+

Using the subscriptions.getById().update() you can update one of the Subscriptions

+
import { graph } from "@pnp/graph";
+
+const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: "created,updated,deleted" });
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/graph/docs/teams/index.html b/v1/graph/docs/teams/index.html new file mode 100644 index 000000000..bc831efd9 --- /dev/null +++ b/v1/graph/docs/teams/index.html @@ -0,0 +1,2097 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + teams - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/graph/teams

+

The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described +you can add, update and delete items in Teams.

+

Teams the user is a member of

+
import { graph } from "@pnp/graph";
+
+const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams.get();
+
+const myJoinedTeams = await graph.me.joinedTeams.get();
+
+ + +

Get Teams by Id

+

Using the teams.getById() you can get a specific Team.

+
import { graph } from "@pnp/graph";
+
+const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').get();
+
+ + +

Create new Group and Team

+

When you create a new group and add a Team, the group needs to have an Owner. Or else we get an error. +So the owner Id is important, and you could just get the users Ids from

+
import { graph } from "@pnp/graph";
+
+const users = await graph.users.get();
+
+ + +

Then create

+
import { graph } from "@pnp/graph";
+
+const createdGroupTeam = await graph.teams.create('Groupname', 'mailNickname', 'description', 'OwnerId',{ 
+"memberSettings": {
+    "allowCreateUpdateChannels": true
+},
+"messagingSettings": {
+        "allowUserEditMessages": true,
+"allowUserDeleteMessages": true
+},
+"funSettings": {
+    "allowGiphy": true,
+    "giphyContentRating": "strict"
+}});
+
+ + +

Create a Team via a specific group

+

Here we get the group via id and use createTeam

+
import { graph } from "@pnp/graph";
+
+const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ 
+"memberSettings": {
+    "allowCreateUpdateChannels": true
+},
+"messagingSettings": {
+        "allowUserEditMessages": true,
+"allowUserDeleteMessages": true
+},
+"funSettings": {
+    "allowGiphy": true,
+    "giphyContentRating": "strict"
+}});
+
+ + +

Archive a Team

+
import { graph } from "@pnp/graph";
+
+const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();
+
+ + +

Unarchive a Team

+
import { graph } from "@pnp/graph";
+
+const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();
+
+ + +

Clone a Team

+
import { graph } from "@pnp/graph";
+
+const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
+'Cloned','mailNickname','description','apps,tabs,settings,channels,members','public');
+
+ + +

Get all channels of a Team

+
import { graph } from "@pnp/graph";
+
+const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.get();
+
+ + +

Get channel by Id

+
import { graph } from "@pnp/graph";
+
+const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').get();
+
+ + +

Create a new Channel

+
import { graph } from "@pnp/graph";
+
+const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');
+
+ + +

Get installed Apps

+
import { graph } from "@pnp/graph";
+
+const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.get();
+
+ + +

Add an App

+
import { graph } from "@pnp/graph";
+
+const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');
+
+ + +

Remove an App

+
import { graph } from "@pnp/graph";
+
+const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove();
+
+ + +

Get Tabs from a Channel

+
import { graph } from "@pnp/graph";
+
+const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs
+.get();
+
+ + +

Get Tab by Id

+
import { graph } from "@pnp/graph";
+
+const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs
+.getById('Id');
+
+ + +

Add a new Tab

+
import { graph } from "@pnp/graph";
+
+const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
+channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/index.html b/v1/index.html new file mode 100644 index 000000000..fb758621a --- /dev/null +++ b/v1/index.html @@ -0,0 +1,1889 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

Home

+ +

SharePoint Patterns and Practices Logo

+

PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.

+

Fluent API in action +Animation of the library in use, note intellisense help in building your queries

+

General Guidance

+

These articles provide general guidance for working with the libraries. If you are migrating from sp-pnp-js please review the transition guide.

+ +

Packages

+

Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
@pnp/
commonProvides shared functionality across all pnp libraries
config-storeProvides a way to manage configuration within your application
graphProvides a fluent api for working with Microsoft Graph
loggingLight-weight, subscribable logging framework
nodejsProvides functionality enabling the @pnp libraries within nodejs
odataProvides shared odata functionality and base classes
pnpjsRollup library of core functionality (mimics sp-pnp-js)
spProvides a fluent api for working with SharePoint REST
sp-addinhelpersProvides functionality for working within SharePoint add-ins
sp-clientsvcProvides based classes used to create a fluent api for working with SharePoint Managed Metadata
sp-taxonomyProvides a fluent api for working with SharePoint Managed Metadata
+

Issues, Questions, Ideas

+

Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any contructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.

+

Code of Conduct

+

This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

+

"Sharing is Caring"

+

Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program.

+

Disclaimer

+

THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + diff --git a/v1/logging/docs/index.html b/v1/logging/docs/index.html new file mode 100644 index 000000000..0c5bf871e --- /dev/null +++ b/v1/logging/docs/index.html @@ -0,0 +1,2045 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + logging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/logging

+

npm version

+

The logging module provides light weight subscribable and extensiable logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.

+

Getting Started

+

Install the logging module, it has no other dependencies

+

npm install @pnp/logging --save

+

Understanding the Logging Framework

+

The logging framework is based on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the LogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface.

+
/**
+ * Interface that defines a log listener
+ *
+ */
+export interface LogListener {
+    /**
+     * Any associated data that a given logging listener may choose to log or ignore
+     *
+     * @param entry The information to be logged
+     */
+    log(entry: LogEntry): void;
+}
+
+/**
+ * Interface that defines a log entry
+ *
+ */
+export interface LogEntry {
+    /**
+     * The main message to be logged
+     */
+    message: string;
+    /**
+     * The level of information this message represents
+     */
+    level: LogLevel;
+    /**
+     * Any associated data that a given logging listener may choose to log or ignore
+     */
+    data?: any;
+}
+
+ + +

Log Levels

+
export const enum LogLevel {
+    Verbose = 0,
+    Info = 1,
+    Warning = 2,
+    Error = 3,
+    Off = 99,
+}
+
+ + +

Writing to the Logger

+

To write information to a logger you can use either write, writeJSON, or log.

+
import {
+    Logger,
+    LogLevel
+} from "@pnp/logging";
+
+// write logs a simple string as the message value of the LogEntry
+Logger.write("This is logging a simple string");
+
+// optionally passing a level, default level is Verbose
+Logger.write("This is logging a simple string", LogLevel.Error);
+
+// this will convert the object to a string using JSON.stringify and set the message with the result
+Logger.writeJSON({ name: "value", name2: "value2"});
+
+// optionally passing a level, default level is Verbose
+Logger.writeJSON({ name: "value", name2: "value2"}, LogLevel.Warn);
+
+// specify the entire LogEntry interface using log
+Logger.log({
+    data: { name: "value", name2: "value2"},
+    level: LogLevel.Warning,
+    message: "This is my message"
+});
+
+ + +

Log an error

+

There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error +instance pased in, the level will be Error, and the message will be the Error instance message.

+
const e = new Error("An Error");
+
+Logger.error(e);
+
+ + +

Subscribing a Listener

+

By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.

+
import {
+    Logger,
+    ConsoleListener,
+    LogLevel
+} from "@pnp/logging";
+
+// subscribe a listener
+Logger.subscribe(new ConsoleListener());
+
+// set the active log level
+Logger.activeLogLevel = LogLevel.Info;
+
+ + +

Available Listeners

+

There are two listeners included in the library, ConsoleListener and FunctionListener.

+

ConsoleListener

+

This listener outputs information to the console and works in Node as well as within browsers. It takes no settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Usage is shown in the example above.

+

FunctionListener

+

The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.

+
import {
+    Logger,
+    FunctionListener,
+    LogEntry
+} from "@pnp/logging";
+
+let listener = new FunctionListener((entry: LogEntry) => {
+
+    // pass all logging data to an existing framework
+    MyExistingCompanyLoggingFramework.log(entry.message);
+});
+
+Logger.subscribe(listener);
+
+ + +

Create a Custom Listener

+

If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the LogListener interface.

+
import {
+    Logger,
+    LogListener,
+    LogEntry
+} from "@pnp/logging";
+
+class MyListener implements LogListener {
+
+    log(entry: LogEntry): void {
+        // here you would do something with the entry
+    }    
+}
+
+Logger.subscribe(new MyListener());
+
+ + +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/logging. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/adal-certificate-fetch-client/index.html b/v1/nodejs/docs/adal-certificate-fetch-client/index.html new file mode 100644 index 000000000..3d23a4e7a --- /dev/null +++ b/v1/nodejs/docs/adal-certificate-fetch-client/index.html @@ -0,0 +1,1676 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @pnp/nodejs/adalcertificatefetchclient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs/adalcertificatefetchclient

+

The AdalCertificateFetchClient class depends on the adal-node package to authenticate against Azure AD using the client credentials with a client certificate flow. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected.

+
import { AdalCertificateFetchClient } from "@pnp/nodejs";
+import { graph } from "@pnp/graph";
+import * as fs from "fs";
+import * as path from "path";
+
+// Get the private key from a file (Assuming it's a .pem file)
+const keyPemFile = "/path/to/privatekey.pem";
+const privateKey = fs.readFileSync(
+    path.resolve(__dirname, keyPemFile), 
+    { encoding : 'utf8'}
+);
+
+// setup the client using graph setup function
+graph.setup({
+    graph: {
+        fetchClientFactory: () => {
+            return new AdalCertificateFetchClient(
+                "{tenant id}", 
+                "{app id}", 
+                "{certificate thumbprint}",
+                privateKey);
+        },
+    },
+});
+
+// execute a library request as normal
+graph.groups.get().then(g => {
+
+    console.log(JSON.stringify(g, null, 4));
+
+}).catch(e => {
+
+    console.error(e);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/adal-fetch-client/index.html b/v1/nodejs/docs/adal-fetch-client/index.html new file mode 100644 index 000000000..9464cfdec --- /dev/null +++ b/v1/nodejs/docs/adal-fetch-client/index.html @@ -0,0 +1,1713 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + AdalFetchClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs/adalfetchclient

+

The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected.

+
import { AdalFetchClient } from "@pnp/nodejs";
+import { graph } from "@pnp/graph";
+
+// setup the client using graph setup function
+graph.setup({
+    graph: {
+        fetchClientFactory: () => {
+            return new AdalFetchClient("{tenant}", "{app id}", "{app secret}");
+        },
+    },
+});
+
+// execute a library request as normal
+graph.groups.get().then(g => {
+
+    console.log(JSON.stringify(g, null, 4));
+
+}).catch(e => {
+
+    console.error(e);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/bearer-token-fetch-client/index.html b/v1/nodejs/docs/bearer-token-fetch-client/index.html new file mode 100644 index 000000000..f35860890 --- /dev/null +++ b/v1/nodejs/docs/bearer-token-fetch-client/index.html @@ -0,0 +1,1712 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + BearerTokenFetchClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs/BearerTokenFetchClient

+

The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you.

+
import { BearerTokenFetchClient } from "@pnp/nodejs";
+import { graph } from "@pnp/graph";
+
+// setup the client using graph setup function
+graph.setup({
+    graph: {
+        fetchClientFactory: () => {
+            return new BearerTokenFetchClient("{Bearer Token}");
+        },
+    },
+});
+
+// execute a library request as normal
+graph.groups.get().then(g => {
+
+    console.log(JSON.stringify(g, null, 4));
+
+}).catch(e => {
+
+    console.error(e);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/index.html b/v1/nodejs/docs/index.html new file mode 100644 index 000000000..5452b0aa1 --- /dev/null +++ b/v1/nodejs/docs/index.html @@ -0,0 +1,1764 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + nodejs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs

+

npm version

+

This package supplies helper code when using the @pnp libraries within the context of nodejs. This removes the node specific functionality from any of the packages. +Primarily these consist of clients to enable use of the libraries in nodejs.

+

Getting Started

+

Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the +exported functionality.

+

npm install @pnp/logging @pnp/core @pnp/nodejs --save

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/nodejs. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/provider-hosted-app/index.html b/v1/nodejs/docs/provider-hosted-app/index.html new file mode 100644 index 000000000..81c1bd022 --- /dev/null +++ b/v1/nodejs/docs/provider-hosted-app/index.html @@ -0,0 +1,1725 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ProviderHostedRequestContext - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs/providerhostedrequestcontext

+

Added in 1.2.7

+

The ProviderHostedRequestcontext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user.

+

The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI.

+

Note: To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context.

+
import { sp, SPRest } from "@pnp/sp";
+import { NodeFetchClient, ProviderHostedRequestContext } from "@pnp/nodejs";
+
+// configure your node options
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new NodeFetchClient();
+        },
+    },
+});
+
+// get request data generated by /_layouts/15/AppRedirect.aspx
+const spAppToken = request.body.SPAppToken;
+const spSiteUrl = request.body.SPSiteUrl;
+
+// create a context based on the add-in details and SPAppToken
+const ctx = await ProviderHostedRequestContext.create(spSiteUrl, "{client id}", "{client secret}", spAppToken);
+
+// create an SPRest object configured to use our context
+// this is used in place of the global sp object
+const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl);
+const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl);
+
+// make a request on behalf of the user
+const user = await userSP.web.currentUser.get();
+console.log(`Hello ${user.Title}`);
+
+// make an add-in only request
+const app = await addinSP.web.currentUser.get();
+console.log(`Add-in principal: ${app.Title}`);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/proxy/index.html b/v1/nodejs/docs/proxy/index.html new file mode 100644 index 000000000..cc1cd6b03 --- /dev/null +++ b/v1/nodejs/docs/proxy/index.html @@ -0,0 +1,1699 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @pnp/nodejs/proxy - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs/proxy

+

Added in 1.3.2

+

In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler. In the 1.3.2 relesae we introduced the ability to use a proxy with the @pnp/nodejs library.

+

Basic Usage

+

You need to import the new setProxyUrl function from the library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library.

+
import { SPFetchClient, SPOAuthEnv, setProxyUrl } from "@pnp/nodejs";
+
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+
+            // call the set proxy url function and it will be used for all requests regardless of client
+            setProxyUrl("{your proxy url}");
+            return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO);
+        },
+    },
+});
+
+ + +

Use with Fiddler

+

To get Fiddler to work you may need to set an environment variable. This should only be done for testing!

+
import { SPFetchClient, SPOAuthEnv, setProxyUrl } from "@pnp/nodejs";
+
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+
+            // ignore certificate errors: ONLY FOR TESTING!!
+            process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
+
+            // this is my fiddler url locally
+            setProxyUrl("http://127.0.0.1:8888");
+            return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO);
+        },
+    },
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/nodejs/docs/sp-fetch-client/index.html b/v1/nodejs/docs/sp-fetch-client/index.html new file mode 100644 index 000000000..cb8b7d13d --- /dev/null +++ b/v1/nodejs/docs/sp-fetch-client/index.html @@ -0,0 +1,1895 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SPFetchClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/nodejs/spfetchclient

+

The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively.

+
import { SPFetchClient } from "@pnp/nodejs";
+import { sp } from "@pnp/sp";
+
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPFetchClient("{site url}", "{client id}", "{client secret}");
+        },
+    },
+});
+
+// execute a library request as normal
+sp.web.get().then(w => {
+
+    console.log(JSON.stringify(w, null, 4));
+
+}).catch(e => {
+
+    console.error(e);
+});
+
+ + +

Set Authentication Environment

+

Added in 1.1.2

+

For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK.

+
    +
  • SPO : (default) for all *.sharepoint.com urls
  • +
  • China: for China hosted cloud
  • +
  • Germany: for Germany local cloud
  • +
  • USDef: USA Defense cloud
  • +
  • USGov: USA Government cloud
  • +
+
import { sp } from "@pnp/sp";
+import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs";
+
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.China);
+        },
+    },
+});
+
+ + +

Set Realm

+

In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to "https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx" and copying the GUID value that appears after the "@" - this is the realm id.

+

As of version 1.1.2 the realm parameter is now the 5th parameter in the constructor.

+
import { sp } from "@pnp/sp";
+import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs";
+
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.SPO, "{realm}");
+        },
+    },
+});
+
+ + +

Creating a client id and secret

+

This section outlines how to register for a client id and secret for use in the above code.

+

Register An Add-In

+

Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly.

+
    +
  1. Navigation to {site url}/_layouts/appregnew.aspx
  2. +
  3. Click "Generate" for both the Client Id and Secret values
  4. +
  5. Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions
  6. +
  7. Provide a fake value for app domain and redirect uri, you can use the values shown in the examples
  8. +
  9. Click "Create"
  10. +
  11. Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.
  12. +
+

Grant Your Add-In Permissions

+

Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site.

+
    +
  1. Navigate to {admin site url}/_layouts/appinv.aspx
  2. +
  3. Paste your client id from the above section into the Add Id box and click "Lookup"
  4. +
  5. You should see the information populated into the form from the last section, if not ensure you have the correct id value
  6. +
  7. Paste the below XML into the permissions request xml box and hit "Create"
  8. +
  9. You should get a confirmation message.
  10. +
+
  <AppPermissionRequests AllowAppOnlyPolicy="true">
+    <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" />
+    <AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" />
+    <AppPermissionRequest Scope="http://sharepoint/search" Right="QueryAsUserIgnoreAppPrincipal" />
+  </AppPermissionRequests>
+
+ + +

Note that the above XML will grant full tenant control, you should grant only those permissions necessary for your application

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/caching/index.html b/v1/odata/docs/caching/index.html new file mode 100644 index 000000000..be871e333 --- /dev/null +++ b/v1/odata/docs/caching/index.html @@ -0,0 +1,1988 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + caching - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+ +
+ + +
+
+ + + + + +

@pnp/queryable/caching

+

Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests.

+

The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph.

+

Basic example

+

You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The below code will get the items from the list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() should always be the last method in the chain before the get() (OR if you are using [[batching]] these methods can be transposed, more details below).

+
import { sp } from "@pnp/sp";
+
+sp.web.lists.getByTitle("Tasks").items.usingCaching().get().then(r => {
+    console.log(r)
+});
+
+sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching().get().then(r => {
+    console.log(r)
+});
+
+ + +

Globally Configure Cache Settings

+

If you would like to not use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application.

+
import { sp } from "@pnp/sp";
+
+sp.setup({
+    defaultCachingStore: "session", // or "local"
+    defaultCachingTimeoutSeconds: 30,
+    globalCacheDisable: false // or true to disable caching in case of debugging/testing
+});
+
+sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching().get().then(r => {
+    console.log(r)
+});
+
+ + +

Per Call Configuration

+

If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key.

+
export interface ICachingOptions {
+    expiration?: Date;
+    storeName?: "session" | "local";
+    key: string;
+}
+
+ + +
import { sp } from "@pnp/sp";
+import { dateAdd } from "@pnp/core";
+
+sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching({
+    expiration: dateAdd(new Date(), "minute", 20),
+    key: "My Key",
+    storeName: "local"
+}).get().then(r => {
+    console.log(r)
+});
+
+ + +

Using Batching with Caching

+

You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid.

+
import { sp } from "@pnp/sp";
+
+let batch = sp.createBatch();
+
+sp.web.lists.inBatch(batch).usingCaching().get().then(r => {
+    console.log(r)
+});
+
+sp.web.lists.getByTitle("Tasks").items.usingCaching().inBatch(batch).get().then(r => {
+    console.log(r)
+});
+
+batch.execute().then(() => console.log("All done!"));
+
+ + +

Implement Custom Caching

+

You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here.

+

Implement caching helper method:

+

We create a map to act as our cache storage and a function to wrap the request caching logic

+
const map = new Map<string, any>();
+
+async function staleWhileRevalidate<T>(key: string, p: Promise<T>): Promise<T> {
+
+    if (map.has(key)) {
+
+        // In Cache
+        p.then(u => {
+            // Update Cache once we have a result
+            map.set(key, u);
+        });
+
+        // Return from Cache
+        return map.get(key);
+    }
+
+    // Not In Cache so we need to wait for the value
+    const r = await p;
+
+    // Set Cache
+    map.set(key, r);
+
+    // Return from Promise
+    return r;
+}
+
+ + +

Usage

+
+

Don't call usingCaching just apply the helper method

+
+
// this one will wait for the request to finish
+const r1 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description").get());
+
+console.log(JSON.stringify(r1, null, 2));
+
+// this one will return the result from cache and then update the cache in the background
+const r2 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description").get());
+
+console.log(JSON.stringify(r2, null, 2));
+
+ + +

Wrapper Function

+

You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well.

+
interface WebData {
+    Title: string;
+    Description: string;
+}
+
+function getWebData(): Promise<WebData> {
+
+    return staleWhileRevalidate("test1", sp.web.select("Title", "Description").get());
+}
+
+
+// this one will wait for the request to finish
+const r1 = await getWebData();
+
+console.log(JSON.stringify(r1, null, 2));
+
+// this one will return the result from cache and then update the cache in the background
+const r2 = await getWebData();
+
+console.log(JSON.stringify(r2, null, 2));
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/core/index.html b/v1/odata/docs/core/index.html new file mode 100644 index 000000000..ac5184726 --- /dev/null +++ b/v1/odata/docs/core/index.html @@ -0,0 +1,1829 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + core - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/queryable/core

+

This modules contains shared interfaces and abstract classes used within, and by inheritors of, the @pnp/queryable package.

+

ProcessHttpClientResponseException

+

The exception thrown when a response is returned and cannot be processed.

+

interface ODataParser

+

Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the +value to be returned. It has two methods, one is optional:

+
    +
  • parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T
  • +
  • hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor
  • +
+

ODataParserBase

+

The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper +methods.

+

Create a custom parser from ODataParserBase

+

You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most +cases.

+
class MyParser extends ODataParserBase<any> {
+
+    // we need to override the parse method to do our custom stuff
+    public parse(r: Response): Promise<T> {
+
+        // we wrap everything in a promise
+        return new Promise((resolve, reject) => {
+
+            // lets use the default error handling which returns true for no error
+            // and will call reject with an error if one exists
+            if (this.handleError(r, reject)) {
+
+                // now we add our custom parsing here
+                r.text().then(txt => {
+                    // here we call a madeup function to parse the result
+                    // this is where we would do our parsing as required
+                    myCustomerUnencode(txt).then(v => {
+                        resolve(v);
+                    });
+                });
+            }
+        });
+    }
+}
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/index.html b/v1/odata/docs/index.html new file mode 100644 index 000000000..c62404aba --- /dev/null +++ b/v1/odata/docs/index.html @@ -0,0 +1,1781 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + odata - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/queryable

+

npm version

+

This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata +library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure +the core code is solid and well tested, with any updates benefitting all inheriting libraries.

+

Getting Started

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable --save

+

Library Topics

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/queryable. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/odata-batch/index.html b/v1/odata/docs/odata-batch/index.html new file mode 100644 index 000000000..44d98f193 --- /dev/null +++ b/v1/odata/docs/odata-batch/index.html @@ -0,0 +1,1755 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + OData Batching - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/queryable/odatabatch

+

This module contains an abstract class used as a base when inheriting libraries support batching.

+

ODataBatchRequestInfo

+

This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will +be responsible for processing that info by implementing the abstract executeImpl method.

+

ODataBatch

+

Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp +and @pnp/graph modules.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/parsers/index.html b/v1/odata/docs/parsers/index.html new file mode 100644 index 000000000..0f9240c30 --- /dev/null +++ b/v1/odata/docs/parsers/index.html @@ -0,0 +1,1862 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Parsers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/queryable/parsers

+

This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need.

+

ODataDefaultParser

+

The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request.

+
import { sp } from "@pnp/sp";
+import { JSONParser } from "@pnp/queryable";
+
+try {
+
+    const parser = new JSONParser();
+
+    // this always throws a 404 error
+    await sp.web.getList("doesn't exist").get(parser);
+
+} catch (e) {
+
+    // we can check for the property "isHttpRequestError" to see if this is an instance of our class
+    // this gets by all the many limitations of subclassing Error and type detection in JavaScript
+    if (e.hasOwnProperty("isHttpRequestError")) {
+
+        console.log("e is HttpRequestError");
+
+        // now we can access the various properties and make use of the response object.
+        // at this point the body is unread
+        console.log(`status: ${e.status}`);
+        console.log(`statusText: ${e.statusText}`);
+
+        const json = await e.response.clone().json();
+        console.log(JSON.stringify(json));
+        const text = await e.response.clone().text();
+        console.log(text);
+        const headers = e.response.headers;
+    }
+
+    console.error(e);
+}
+
+ + +

TextParser

+

Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files.

+

BlobParser

+

Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files.

+

JSONParser

+

Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files.

+

BufferParser

+

Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files.

+

LambdaParser

+

Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type.

+
import { LambdaParser } from "@pnp/queryable";
+import { sp } from "@pnp/sp";
+
+// here a simple parser duplicating the functionality of the JSONParser
+const parser = new LambdaParser((r: Response) => r.json());
+
+const webDataJson = await sp.web.get(parser);
+
+console.log(webDataJson);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/pipeline/index.html b/v1/odata/docs/pipeline/index.html new file mode 100644 index 000000000..31d683c35 --- /dev/null +++ b/v1/odata/docs/pipeline/index.html @@ -0,0 +1,1811 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Pipeline - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/queryable/pipeline

+

All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by +the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the +pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline.

+

interface RequestContext

+

The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This +allows full control over the methods called during a request, and allows for the insertion of any custom methods required.

+
interface RequestContext<T> {
+    batch: ODataBatch;
+    batchDependency: () => void;
+    cachingOptions: ICachingOptions;
+    hasResult?: boolean;
+    isBatched: boolean;
+    isCached: boolean;
+    options: FetchOptions;
+    parser: ODataParser<T>;
+    pipeline: Array<(c: RequestContext<T>) => Promise<RequestContext<T>>>;
+    requestAbsoluteUrl: string;
+    requestId: string;
+    result?: T;
+    verb: string;
+    clientFactory: () => RequestClient;
+}
+
+ + +

requestPipelineMethod decorator

+

The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you +would like your method to always run regardless of the existance of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument +of the current RequestContext and returns a promise resolving to the RequestContext updated as needed.

+
@requestPipelineMethod(true)
+public static myPipelineMethod<T>(context: RequestContext<T>): Promise<RequestContext<T>> {
+
+    return new Promise<RequestContext<T>>(resolve => {
+
+        // do something
+
+        resolve(context);
+    });
+}
+
+ + +

Default Pipeline

+
    +
  1. logs the start of the request
  2. +
  3. checks the cache for a value based on the context's cache settings
  4. +
  5. sends the request if no value from found in the cache
  6. +
  7. logs the end of the request
  8. +
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/odata/docs/queryable/index.html b/v1/odata/docs/queryable/index.html new file mode 100644 index 000000000..468188ac4 --- /dev/null +++ b/v1/odata/docs/queryable/index.html @@ -0,0 +1,1967 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Queryable - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/queryable/queryable

+

The Queryable class is the base class for all of the libraries building fluent request apis.

+

abstract class ODataQueryable

+

This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching +you can create a dummy class here and simply not use the batching calls.

+

properties

+

query

+

Provides access to the query string builder for this url

+

public methods

+

concat

+

Directly concatenates the supplied string to the current url, not normalizing "/" chars

+

configure

+

Sets custom options for current object and all derived objects accessible via chaining

+
import { ConfigOptions } from "@pnp/queryable";
+import { sp } from "@pnp/sp";
+
+const headers: ConfigOptions = {
+    Accept: 'application/json;odata=nometadata'
+};
+
+// here we use configure to set the headers value for all child requests of the list instance
+const list = sp.web.lists.getByTitle("List1").configure({ headers });
+
+// this will use the values set in configure
+list.items.get().then(items => console.log(JSON.stringify(items, null, 2));
+
+ + +

For reference the ConfigOptions interface is shown below:

+
export interface ConfigOptions {
+    headers?: string[][] | { [key: string]: string } | Headers;
+    mode?: "navigate" | "same-origin" | "no-cors" | "cors";
+    credentials?: "omit" | "same-origin" | "include";
+    cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached";
+}
+
+ + +

configureFrom

+

Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance.

+

usingCaching

+

Enables caching for this request. See caching for more details.

+
import { sp } from "@pnp/sp"
+
+sp.web.usingCaching().get().then(...);
+
+ + +

inBatch

+

Adds this query to the supplied batch

+

toUrl

+

Gets the current url

+

abstract toUrlAndQuery()

+

When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request

+

get

+

Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/pnpjs/docs/index.html b/v1/pnpjs/docs/index.html new file mode 100644 index 000000000..e659ea737 --- /dev/null +++ b/v1/pnpjs/docs/index.html @@ -0,0 +1,1814 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + pnpjs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/pnpjs

+

npm version

+

The pnpjs library is a rollup of the core libraries across the @pnp scope and is designed only as a bridge to help folks transition from sp-pnp-js, primarily +in scenarios where a single file is being imported via a script tag. It is recommended to not use this rollup library where possible and migrate to the +individual libraries.

+

Getting Started

+

There are two approaches to using this library: the first is to import, the second is to manually extract the bundled file for use in your project.

+

Install

+

npm install @pnp/pnpjs --save

+

You can then make use of the pnpjs rollup library within your application. It's structure matches sp-pnp-js, though some things may have changed based on the rolled-up dependencies.

+
import pnp from "@pnp/pnpjs";
+
+pnp.sp.web.get().then(w => {
+
+    console.log(JSON.stringify(w, null, 4));
+});
+
+ + +

Grab Bundle File

+

This method is useful if you are primarily working within a script editor web part or similar case where you are not using a build pipeline to bundle your application.

+

Install only this library.

+

npm install @pnp/pnpjs

+

Browse to ./node_modules/@pnp/pnpjs/dist and grab either pnpjs.es5.umd.bundle.js or pnpjs.es5.umd.bundle.min.js depending on your needs. You can then add a script tag referencing this file and you will have a global variable "pnp".

+

For example you could paste the following into a script editor web part:

+
<p>Script Editor is on page.</p>
+<script src="https://mysite/site_assets/pnpjs.es5.umd.bundle.min.js" type="text/javascript"></script>
+<script type="text/javascript">
+
+    pnp.Logger.subscribe(new pnp.ConsoleListener());
+    pnp.Logger.activeLogLevel = pnp.LogLevel.Info;
+
+    pnp.sp.web.get().then(w => {
+
+        console.log(JSON.stringify(w, null, 4));
+    });
+</script>
+
+ + +

Alternatively to serve the script from the project at "https://localhost:8080/assets/pnp.js" you can use:

+

gulp serve --p pnpjs

+

This will allow you to test your changes to the entire bundle live while making updates.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/search/search_index.json b/v1/search/search_index.json new file mode 100644 index 000000000..586b32d18 --- /dev/null +++ b/v1/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community. Animation of the library in use, note intellisense help in building your queries General Guidance \u00b6 These articles provide general guidance for working with the libraries. If you are migrating from sp-pnp-js please review the transition guide . Getting Started Getting Started Contributing Documentation Gulp Commands Debugging Deployment Install Beta Versions Polyfills Package Structure Packages \u00b6 Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. @pnp/ common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes pnpjs Rollup library of core functionality (mimics sp-pnp-js) sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins sp-clientsvc Provides based classes used to create a fluent api for working with SharePoint Managed Metadata sp-taxonomy Provides a fluent api for working with SharePoint Managed Metadata Issues, Questions, Ideas \u00b6 Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any contructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project. Code of Conduct \u00b6 This project has adopted the Microsoft Open Source Code of Conduct . For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments. \"Sharing is Caring\" \u00b6 Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program . Disclaimer \u00b6 THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.","title":"Home"},{"location":"#general-guidance","text":"These articles provide general guidance for working with the libraries. If you are migrating from sp-pnp-js please review the transition guide . Getting Started Getting Started Contributing Documentation Gulp Commands Debugging Deployment Install Beta Versions Polyfills Package Structure","title":"General Guidance"},{"location":"#packages","text":"Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. @pnp/ common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes pnpjs Rollup library of core functionality (mimics sp-pnp-js) sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins sp-clientsvc Provides based classes used to create a fluent api for working with SharePoint Managed Metadata sp-taxonomy Provides a fluent api for working with SharePoint Managed Metadata","title":"Packages"},{"location":"#issues-questions-ideas","text":"Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any contructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.","title":"Issues, Questions, Ideas"},{"location":"#code-of-conduct","text":"This project has adopted the Microsoft Open Source Code of Conduct . For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.","title":"Code of Conduct"},{"location":"#sharing-is-caring","text":"Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program .","title":"\"Sharing is Caring\""},{"location":"#disclaimer","text":"THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.","title":"Disclaimer"},{"location":"common/docs/","text":"@pnp/core \u00b6 The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/core --save Import and use functionality, see details on modules below. import { getGUID } from \"@pnp/core\" ; console . log ( getGUID ()); Exports \u00b6 adalclient collections libconfig netutil storage util Custom HttpClient UML \u00b6 Graphical UML diagram of @pnp/core. Right-click the diagram and open in new tab if it is too small.","title":"common"},{"location":"common/docs/#pnpcommon","text":"The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well.","title":"@pnp/core"},{"location":"common/docs/#getting-started","text":"Install the library and required dependencies npm install @pnp/core --save Import and use functionality, see details on modules below. import { getGUID } from \"@pnp/core\" ; console . log ( getGUID ());","title":"Getting Started"},{"location":"common/docs/#exports","text":"adalclient collections libconfig netutil storage util Custom HttpClient","title":"Exports"},{"location":"common/docs/#uml","text":"Graphical UML diagram of @pnp/core. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"common/docs/adalclient/","text":"@pnp/core/adalclient \u00b6 Added in 1.0.4 This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions. Setup and Use inside SharePoint Framework \u00b6 Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes . This method only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined above using the constructor to specify the values for an AAD Application you have setup. Calling the graph api \u00b6 By providing the context in the onInit we can create the adal client from known information. import { graph } from \"@pnp/graph\" ; import { getRandomString } from \"@pnp/core\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present graph . setup ({ spfxContext : this.context }); }); } public render () : void { // here we are creating a team with a random name, required Group ReadWrite All permissions const teamName = `ATeam. ${ getRandomString ( 4 ) } ` ; this . domElement . innerHTML = `Hello, I am creating a team named \" ${ teamName } \" for you...` ; graph . teams . create ( teamName , \"This is a description\" ). then ( t => { this . domElement . innerHTML += \"done!\" ; }). catch ( e => { this . domElement . innerHTML = `Oops, I ran into a problem... ${ JSON . stringify ( e , null , 4 ) } ` ; }); } Calling the SharePoint API \u00b6 This example shows how to use the ADALClient with the @pnp/sp library to call import { sp } from \"@pnp/sp\" ; import { AdalClient } from \"@pnp/core\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present sp . setup ({ spfxContext : this.context , sp : { fetchClientFactory : () => , }, }); }); } public render () : void { sp . web . get (). then ( t => { this . domElement . innerHTML = JSON . stringify ( t ); }). catch ( e => { this . domElement . innerHTML = JSON . stringify ( e ); }); } Calling the any API \u00b6 You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. import { AdalClient , FetchOptions } from \"@pnp/core\" ; import { ODataDefaultParser } from \"@pnp/queryable\" ; // ... public render () : void { // create an ADAL Client const client = AdalClient . fromSPFxContext ( this . context ); // setup the request options const opts : FetchOptions = { method : \"GET\" , headers : { \"Accept\" : \"application/json\" , }, }; // execute the request client . fetch ( \"https://tenant.sharepoint.com/_api/web\" , opts ). then ( response => { // create a parser to convert the response into JSON. // You can create your own, at this point you have a fetch Response to work with const parser = new ODataDefaultParser (); parser . parse ( response ). then ( json => { this . domElement . innerHTML = JSON . stringify ( json ); }); }). catch ( e => { this . domElement . innerHTML = JSON . stringify ( e ); }); } Manually Configure \u00b6 This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD. Setup and Use with Microsoft Graph \u00b6 This sample uses a custom AzureAd app you have created and granted the appropriate permissions. import { AdalClient } from \"@pnp/core\" ; import { graph } from \"@pnp/graph\" ; // configure the graph client // parameters are: // client id - the id of the application you created in azure ad // tenant - can be id or URL (shown) // redirect url - absolute url of a page to which your application and Azure AD app allows replies graph . setup ({ graph : { fetchClientFactory : () => { return new AdalClient ( \"e3e9048e-ea28-423b-aca9-3ea931cc7972\" , \"{tenant}.onmicrosoft.com\" , \"https://myapp/singlesignon.aspx\" ); }, }, }); try { // call the graph API const groups = await graph . groups . get (); console . log ( JSON . stringify ( groups , null , 4 )); } catch ( e ) { console . error ( e ); } Nodejs Applications \u00b6 We have a dedicated node client in @pnp/nodejs.","title":"adalclient"},{"location":"common/docs/adalclient/#pnpcommonadalclient","text":"Added in 1.0.4 This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions.","title":"@pnp/core/adalclient"},{"location":"common/docs/adalclient/#setup-and-use-inside-sharepoint-framework","text":"Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes . This method only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined above using the constructor to specify the values for an AAD Application you have setup.","title":"Setup and Use inside SharePoint Framework"},{"location":"common/docs/adalclient/#calling-the-graph-api","text":"By providing the context in the onInit we can create the adal client from known information. import { graph } from \"@pnp/graph\" ; import { getRandomString } from \"@pnp/core\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present graph . setup ({ spfxContext : this.context }); }); } public render () : void { // here we are creating a team with a random name, required Group ReadWrite All permissions const teamName = `ATeam. ${ getRandomString ( 4 ) } ` ; this . domElement . innerHTML = `Hello, I am creating a team named \" ${ teamName } \" for you...` ; graph . teams . create ( teamName , \"This is a description\" ). then ( t => { this . domElement . innerHTML += \"done!\" ; }). catch ( e => { this . domElement . innerHTML = `Oops, I ran into a problem... ${ JSON . stringify ( e , null , 4 ) } ` ; }); }","title":"Calling the graph api"},{"location":"common/docs/adalclient/#calling-the-sharepoint-api","text":"This example shows how to use the ADALClient with the @pnp/sp library to call import { sp } from \"@pnp/sp\" ; import { AdalClient } from \"@pnp/core\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present sp . setup ({ spfxContext : this.context , sp : { fetchClientFactory : () => , }, }); }); } public render () : void { sp . web . get (). then ( t => { this . domElement . innerHTML = JSON . stringify ( t ); }). catch ( e => { this . domElement . innerHTML = JSON . stringify ( e ); }); }","title":"Calling the SharePoint API"},{"location":"common/docs/adalclient/#calling-the-any-api","text":"You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. import { AdalClient , FetchOptions } from \"@pnp/core\" ; import { ODataDefaultParser } from \"@pnp/queryable\" ; // ... public render () : void { // create an ADAL Client const client = AdalClient . fromSPFxContext ( this . context ); // setup the request options const opts : FetchOptions = { method : \"GET\" , headers : { \"Accept\" : \"application/json\" , }, }; // execute the request client . fetch ( \"https://tenant.sharepoint.com/_api/web\" , opts ). then ( response => { // create a parser to convert the response into JSON. // You can create your own, at this point you have a fetch Response to work with const parser = new ODataDefaultParser (); parser . parse ( response ). then ( json => { this . domElement . innerHTML = JSON . stringify ( json ); }); }). catch ( e => { this . domElement . innerHTML = JSON . stringify ( e ); }); }","title":"Calling the any API"},{"location":"common/docs/adalclient/#manually-configure","text":"This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD.","title":"Manually Configure"},{"location":"common/docs/adalclient/#setup-and-use-with-microsoft-graph","text":"This sample uses a custom AzureAd app you have created and granted the appropriate permissions. import { AdalClient } from \"@pnp/core\" ; import { graph } from \"@pnp/graph\" ; // configure the graph client // parameters are: // client id - the id of the application you created in azure ad // tenant - can be id or URL (shown) // redirect url - absolute url of a page to which your application and Azure AD app allows replies graph . setup ({ graph : { fetchClientFactory : () => { return new AdalClient ( \"e3e9048e-ea28-423b-aca9-3ea931cc7972\" , \"{tenant}.onmicrosoft.com\" , \"https://myapp/singlesignon.aspx\" ); }, }, }); try { // call the graph API const groups = await graph . groups . get (); console . log ( JSON . stringify ( groups , null , 4 )); } catch ( e ) { console . error ( e ); }","title":"Setup and Use with Microsoft Graph"},{"location":"common/docs/adalclient/#nodejs-applications","text":"We have a dedicated node client in @pnp/nodejs.","title":"Nodejs Applications"},{"location":"common/docs/collections/","text":"@pnp/core/collections \u00b6 The collections module provides typings and classes related to working with dictionaries. TypedHash \u00b6 Interface used to described an object with string keys corresponding to values of type T export interface TypedHash < T > { [ key : string ] : T ; } objectToMap \u00b6 Converts a plain object to a Map instance const map = objectToMap ({ a : \"b\" , c : \"d\" }); mergeMaps \u00b6 Merges two or more maps, overwriting values with the same key. Last value in wins. const m1 = new Map (); const m2 = new Map (); const m3 = new Map (); const m4 = new Map (); const m = mergeMaps ( m1 , m2 , m3 , m4 );","title":"collections"},{"location":"common/docs/collections/#pnpcommoncollections","text":"The collections module provides typings and classes related to working with dictionaries.","title":"@pnp/core/collections"},{"location":"common/docs/collections/#typedhash","text":"Interface used to described an object with string keys corresponding to values of type T export interface TypedHash < T > { [ key : string ] : T ; }","title":"TypedHash"},{"location":"common/docs/collections/#objecttomap","text":"Converts a plain object to a Map instance const map = objectToMap ({ a : \"b\" , c : \"d\" });","title":"objectToMap"},{"location":"common/docs/collections/#mergemaps","text":"Merges two or more maps, overwriting values with the same key. Last value in wins. const m1 = new Map (); const m2 = new Map (); const m3 = new Map (); const m4 = new Map (); const m = mergeMaps ( m1 , m2 , m3 , m4 );","title":"mergeMaps"},{"location":"common/docs/custom-httpclientimpl/","text":"Custom HttpClientImpl \u00b6 This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation. It is possible you may need complete control over the sending and receiving of requests. Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation. The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. export interface HttpClientImpl { fetch ( url : string , options : FetchOptions ) : Promise < Response > ; } There is a single method \"fetch\" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. export interface FetchOptions { method? : string ; headers? : HeadersInit | { [ index : string ] : string }; body? : BodyInit ; mode? : string | RequestMode ; credentials? : string | RequestCredentials ; cache? : string | RequestCache ; } So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read \ud83d\udc4d. Using Your Custom HttpClientImpl \u00b6 Once you have written your implementation using it on your requests is done by setting it in the global library configuration: import { setup } from \"@pnp/core\" ; import { sp , Web } from \"@pnp/sp\" ; import { MyAwesomeClient } from \"./awesomeclient\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new MyAwesomeClient (); } } }); let w = new Web ( \"{site url}\" ); // this request will use your client. w . select ( \"Title\" ). get (). then ( w => { console . log ( w ); }); Subclassing is Better \u00b6 You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation. A FINAL NOTE \u00b6 Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.","title":"Custom HttpClientImpl"},{"location":"common/docs/custom-httpclientimpl/#custom-httpclientimpl","text":"This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation. It is possible you may need complete control over the sending and receiving of requests. Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation. The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. export interface HttpClientImpl { fetch ( url : string , options : FetchOptions ) : Promise < Response > ; } There is a single method \"fetch\" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. export interface FetchOptions { method? : string ; headers? : HeadersInit | { [ index : string ] : string }; body? : BodyInit ; mode? : string | RequestMode ; credentials? : string | RequestCredentials ; cache? : string | RequestCache ; } So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read \ud83d\udc4d.","title":"Custom HttpClientImpl"},{"location":"common/docs/custom-httpclientimpl/#using-your-custom-httpclientimpl","text":"Once you have written your implementation using it on your requests is done by setting it in the global library configuration: import { setup } from \"@pnp/core\" ; import { sp , Web } from \"@pnp/sp\" ; import { MyAwesomeClient } from \"./awesomeclient\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new MyAwesomeClient (); } } }); let w = new Web ( \"{site url}\" ); // this request will use your client. w . select ( \"Title\" ). get (). then ( w => { console . log ( w ); });","title":"Using Your Custom HttpClientImpl"},{"location":"common/docs/custom-httpclientimpl/#subclassing-is-better","text":"You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation.","title":"Subclassing is Better"},{"location":"common/docs/custom-httpclientimpl/#a-final-note","text":"Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.","title":"A FINAL NOTE"},{"location":"common/docs/libconfig/","text":"@pnp/core/libconfig \u00b6 Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications. LibraryConfiguration Interface \u00b6 Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below export interface LibraryConfiguration { /** * Allows caching to be global disabled, default: false */ globalCacheDisable? : boolean ; /** * Defines the default store used by the usingCaching method, default: session */ defaultCachingStore ?: \"session\" | \"local\" ; /** * Defines the default timeout in seconds used by the usingCaching method, default 30 */ defaultCachingTimeoutSeconds? : number ; /** * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval */ enableCacheExpiration? : boolean ; /** * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) */ cacheExpirationIntervalMilliseconds? : number ; /** * Used to supply the current context from an SPFx webpart to the library */ spfxContext? : any ; } RuntimeConfigImpl \u00b6 The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method. extend \u00b6 The extend method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below \"Using RuntimeConfig within your application\". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. import { RuntimeConfig } from \"@pnp/core\" ; // add your custom keys to the global configuration // note you can use object hashes as values RuntimeConfig . extend ({ \"myKey1\" : \"value 1\" , \"myKey2\" : { \"subKey\" : \"sub value 1\" , \"subKey2\" : \"sub value 2\" , }, }); // read your custom values const v = RuntimeConfig . get ( \"myKey1\" ); // \"value 1\" Using RuntimeConfig within your Application \u00b6 If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties import { LibraryConfiguration , RuntimeConfig } from \"@pnp/core\" ; // first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because // TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. // create the interface of your properties // by creating this separately you allows others to compose your parts into their own config interface MyConfigurationPart { // you can create a grouped definition and access your settings as an object // keys can be optional or required as defined by your interface my ?: { prop1? : string ; prop2? : string ; } // and/or define multiple top level properties (beware key collision) // it is good practice to use a unique prefix myProp1 : string ; myProp2 : number ; } // now create a combined interface interface MyConfiguration extends LibraryConfiguration , MyConfigurationPart { } // now create a wrapper object and expose your properties class MyRuntimeConfigImpl { // exposing a nested property public get prop1 () : TypedHash < string > { const myPart = RuntimeConfig . get ( \"my\" ); if ( myPart !== null && typeof myPart !== \"undefined\" && typeof myPart . prop1 !== \"undefined\" ) { return myPart . prop1 ; } return {}; } // exposing a root level property public get myProp1 () : string | null { let myProp1 = RuntimeConfig . get ( \"myProp1\" ); if ( myProp1 === null ) { myProp1 = \"some default value\" ; } return myProp1 ; } setup ( config : MyConfiguration ) : void { RuntimeConfig . extend ( config ); } } // create a single static instance of your impl class export let MyRuntimeConfig = new MyRuntimeConfigImpl (); Now in other files you can use and set your configuration with a typed interface and properties import { MyRuntimeConfig } from \"{location of module}\" ; MyRuntimeConfig . setup ({ my : { prop1 : \"hello\" , }, }); const value = MyRuntimeConfig . prop1 ; // \"hello\"","title":"libconfig"},{"location":"common/docs/libconfig/#pnpcommonlibconfig","text":"Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications.","title":"@pnp/core/libconfig"},{"location":"common/docs/libconfig/#libraryconfiguration-interface","text":"Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below export interface LibraryConfiguration { /** * Allows caching to be global disabled, default: false */ globalCacheDisable? : boolean ; /** * Defines the default store used by the usingCaching method, default: session */ defaultCachingStore ?: \"session\" | \"local\" ; /** * Defines the default timeout in seconds used by the usingCaching method, default 30 */ defaultCachingTimeoutSeconds? : number ; /** * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval */ enableCacheExpiration? : boolean ; /** * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) */ cacheExpirationIntervalMilliseconds? : number ; /** * Used to supply the current context from an SPFx webpart to the library */ spfxContext? : any ; }","title":"LibraryConfiguration Interface"},{"location":"common/docs/libconfig/#runtimeconfigimpl","text":"The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method.","title":"RuntimeConfigImpl"},{"location":"common/docs/libconfig/#extend","text":"The extend method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below \"Using RuntimeConfig within your application\". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. import { RuntimeConfig } from \"@pnp/core\" ; // add your custom keys to the global configuration // note you can use object hashes as values RuntimeConfig . extend ({ \"myKey1\" : \"value 1\" , \"myKey2\" : { \"subKey\" : \"sub value 1\" , \"subKey2\" : \"sub value 2\" , }, }); // read your custom values const v = RuntimeConfig . get ( \"myKey1\" ); // \"value 1\"","title":"extend"},{"location":"common/docs/libconfig/#using-runtimeconfig-within-your-application","text":"If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties import { LibraryConfiguration , RuntimeConfig } from \"@pnp/core\" ; // first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because // TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. // create the interface of your properties // by creating this separately you allows others to compose your parts into their own config interface MyConfigurationPart { // you can create a grouped definition and access your settings as an object // keys can be optional or required as defined by your interface my ?: { prop1? : string ; prop2? : string ; } // and/or define multiple top level properties (beware key collision) // it is good practice to use a unique prefix myProp1 : string ; myProp2 : number ; } // now create a combined interface interface MyConfiguration extends LibraryConfiguration , MyConfigurationPart { } // now create a wrapper object and expose your properties class MyRuntimeConfigImpl { // exposing a nested property public get prop1 () : TypedHash < string > { const myPart = RuntimeConfig . get ( \"my\" ); if ( myPart !== null && typeof myPart !== \"undefined\" && typeof myPart . prop1 !== \"undefined\" ) { return myPart . prop1 ; } return {}; } // exposing a root level property public get myProp1 () : string | null { let myProp1 = RuntimeConfig . get ( \"myProp1\" ); if ( myProp1 === null ) { myProp1 = \"some default value\" ; } return myProp1 ; } setup ( config : MyConfiguration ) : void { RuntimeConfig . extend ( config ); } } // create a single static instance of your impl class export let MyRuntimeConfig = new MyRuntimeConfigImpl (); Now in other files you can use and set your configuration with a typed interface and properties import { MyRuntimeConfig } from \"{location of module}\" ; MyRuntimeConfig . setup ({ my : { prop1 : \"hello\" , }, }); const value = MyRuntimeConfig . prop1 ; // \"hello\"","title":"Using RuntimeConfig within your Application"},{"location":"common/docs/netutil/","text":"@pnp/core/netutil \u00b6 This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces are described below (many have no use outside the library) as well as several classes. Interfaces \u00b6 HttpClientImpl \u00b6 Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method \"fetch\" take a URL and options and returning a Promise . Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed. RequestClient \u00b6 An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the underlying HttpClientImpl fetch method. Classes \u00b6 This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl. FetchClient \u00b6 Basic implementation that calls the global (window) fetch method with no additional processing. import { FetchClient } from \"@pnp/core\" ; const client = new FetchClient (); client . fetch ( \"{url}\" , {}); BearerTokenFetchClient \u00b6 A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string. import { BearerTokenFetchClient } from \"@pnp/core\" ; const client = new BearerTokenFetchClient ( \"{authentication token}\" ); client . fetch ( \"{url}\" , {});","title":"netutil"},{"location":"common/docs/netutil/#pnpcommonnetutil","text":"This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces are described below (many have no use outside the library) as well as several classes.","title":"@pnp/core/netutil"},{"location":"common/docs/netutil/#interfaces","text":"","title":"Interfaces"},{"location":"common/docs/netutil/#httpclientimpl","text":"Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method \"fetch\" take a URL and options and returning a Promise . Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed.","title":"HttpClientImpl"},{"location":"common/docs/netutil/#requestclient","text":"An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the underlying HttpClientImpl fetch method.","title":"RequestClient"},{"location":"common/docs/netutil/#classes","text":"This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl.","title":"Classes"},{"location":"common/docs/netutil/#fetchclient","text":"Basic implementation that calls the global (window) fetch method with no additional processing. import { FetchClient } from \"@pnp/core\" ; const client = new FetchClient (); client . fetch ( \"{url}\" , {});","title":"FetchClient"},{"location":"common/docs/netutil/#bearertokenfetchclient","text":"A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string. import { BearerTokenFetchClient } from \"@pnp/core\" ; const client = new BearerTokenFetchClient ( \"{authentication token}\" ); client . fetch ( \"{url}\" , {});","title":"BearerTokenFetchClient"},{"location":"common/docs/storage/","text":"@pnp/core/storage \u00b6 This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below. PnPClientStorage \u00b6 The main export of this module, contains properties representing local and session storage. import { PnPClientStorage } from \"@pnp/core\" ; const storage = new PnPClientStorage (); const myvalue = storage . local . get ( \"mykey\" ); PnPClientStorageWrapper \u00b6 Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. import { PnPClientStorage } from \"@pnp/core\" ; const storage = new PnPClientStorage (); // get a value from storage const value = storage . local . get ( \"mykey\" ); // put a value into storage storage . local . put ( \"mykey2\" , \"my value\" ); // put a value into storage with an expiration storage . local . put ( \"mykey2\" , \"my value\" , new Date ()); // put a simple object into storage // because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects storage . local . put ( \"mykey3\" , { key : \"value\" , key2 : \"value2\" , }); // remove a value from storage storage . local . delete ( \"mykey3\" ); // get an item or add it if it does not exist // returns a promise in case you need time to get the value for storage // optionally takes a third parameter specifying the expiration storage . local . getOrPut ( \"mykey4\" , () => { return Promise . resolve ( \"value\" ); }); // delete expired items storage . local . deleteExpired (); Cache Expiration \u00b6 The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. import { PnPClientStorage } from \"@pnp/core\" ; const storage = new PnPClientStorage (); // session storage storage . session . deleteExpired (); // local storage storage . local . deleteExpired (); // this returns a promise, so you can perform some activity after the expired items are removed: storage . local . deleteExpired (). then ( _ => { // init my application }); The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. import { setup } from \"@pnp/core\" ; setup ({ enableCacheExpiration : true , cacheExpirationIntervalMilliseconds : 1000 , // optional });","title":"storage"},{"location":"common/docs/storage/#pnpcommonstorage","text":"This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.","title":"@pnp/core/storage"},{"location":"common/docs/storage/#pnpclientstorage","text":"The main export of this module, contains properties representing local and session storage. import { PnPClientStorage } from \"@pnp/core\" ; const storage = new PnPClientStorage (); const myvalue = storage . local . get ( \"mykey\" );","title":"PnPClientStorage"},{"location":"common/docs/storage/#pnpclientstoragewrapper","text":"Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. import { PnPClientStorage } from \"@pnp/core\" ; const storage = new PnPClientStorage (); // get a value from storage const value = storage . local . get ( \"mykey\" ); // put a value into storage storage . local . put ( \"mykey2\" , \"my value\" ); // put a value into storage with an expiration storage . local . put ( \"mykey2\" , \"my value\" , new Date ()); // put a simple object into storage // because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects storage . local . put ( \"mykey3\" , { key : \"value\" , key2 : \"value2\" , }); // remove a value from storage storage . local . delete ( \"mykey3\" ); // get an item or add it if it does not exist // returns a promise in case you need time to get the value for storage // optionally takes a third parameter specifying the expiration storage . local . getOrPut ( \"mykey4\" , () => { return Promise . resolve ( \"value\" ); }); // delete expired items storage . local . deleteExpired ();","title":"PnPClientStorageWrapper"},{"location":"common/docs/storage/#cache-expiration","text":"The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. import { PnPClientStorage } from \"@pnp/core\" ; const storage = new PnPClientStorage (); // session storage storage . session . deleteExpired (); // local storage storage . local . deleteExpired (); // this returns a promise, so you can perform some activity after the expired items are removed: storage . local . deleteExpired (). then ( _ => { // init my application }); The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. import { setup } from \"@pnp/core\" ; setup ({ enableCacheExpiration : true , cacheExpirationIntervalMilliseconds : 1000 , // optional });","title":"Cache Expiration"},{"location":"common/docs/util/","text":"@pnp/core/util \u00b6 This module contains utility methods that you can import individually from the common library. import { getRandomString , } from \"@pnp/core\" ; // use from individual;y imported method console . log ( getRandomString ( 10 )); getCtxCallback \u00b6 Gets a callback function which will maintain context across async calls. import { getCtxCallback } from \"@pnp/core\" ; const contextThis = { myProp : 6 , }; function theFunction() { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this . myProp ; } const callback = getCtxCallback ( contextThis , theFunction ); callback (); // returns 6 // You can also supply additional parameters if needed function theFunction2 ( g : number ) { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this . myProp + g ; } const callback2 = getCtxCallback ( contextThis , theFunction , 4 ); callback2 (); // returns 10 (6 + 4) dateAdd \u00b6 Manipulates a date, please see the Stackoverflow discussion from where this method was taken. combine \u00b6 Combines any number of paths, normalizing the slashes as required import { combine } from \"@pnp/core\" ; // \"https://microsoft.com/something/more\" const paths = combine ( \"https://microsoft.com\" , \"something\" , \"more\" ); // \"also/works/with/relative\" const paths2 = combine ( \"/also/\" , \"/works\" , \"with/\" , \"/relative\\\\\" ); getRandomString \u00b6 Gets a random string consisting of the number of characters requested. import { getRandomString } from \"@pnp/core\" ; const randomString = getRandomString ( 10 ); getGUID \u00b6 Creates a random guid, please see the Stackoverflow discussion from where this method was taken. isFunc \u00b6 Determines if a supplied variable represents a function. objectDefinedNotNull \u00b6 Determines if an object is defined and not null. isArray \u00b6 Determines if a supplied variable represents an array. extend \u00b6 Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing properties. import { extend } from \"@pnp/core\" ; let obj1 = { prop : 1 , prop2 : 2 , }; const obj2 = { prop : 4 , prop3 : 9 , }; const example1 = extend ( obj1 , obj2 ); // example1 = { prop: 4, prop2: 2, prop3: 9 } const example2 = extend ( obj1 , obj2 , true ); // example2 = { prop: 1, prop2: 2, prop3: 9 } isUrlAbsolute \u00b6 Determines if a supplied url is absolute and returns true; otherwise returns false. stringIsNullOrEmpty \u00b6 Determines if a supplied string is null or empty Removed \u00b6 Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods below for use in your projects should you require. /** * Loads a stylesheet into the current page * * @param path The url to the stylesheet * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues */ public static loadStylesheet ( path : string , avoidCache : boolean ) : void { if ( avoidCache ) { path += \"?\" + encodeURIComponent (( new Date ()). getTime (). toString ()); } const head = document . getElementsByTagName ( \"head\" ); if ( head . length > 0 ) { const e = document . createElement ( \"link\" ); head [ 0 ]. appendChild ( e ); e . setAttribute ( \"type\" , \"text/css\" ); e . setAttribute ( \"rel\" , \"stylesheet\" ); e . setAttribute ( \"href\" , path ); } } /** * Tests if a url param exists * * @param name The name of the url parameter to check */ public static urlParamExists ( name : string ) : boolean { name = name . replace ( /[\\[]/ , \"\\\\[\" ). replace ( /[\\]]/ , \"\\\\]\" ); const regex = new RegExp ( \"[\\\\?&]\" + name + \"=([^&#]*)\" ); return regex . test ( location . search ); } /** * Gets a url param value by name * * @param name The name of the parameter for which we want the value */ public static getUrlParamByName ( name : string ) : string { name = name . replace ( /[\\[]/ , \"\\\\[\" ). replace ( /[\\]]/ , \"\\\\]\" ); const regex = new RegExp ( \"[\\\\?&]\" + name + \"=([^&#]*)\" ); const results = regex . exec ( location . search ); return results == null ? \"\" : decodeURIComponent ( results [ 1 ]. replace ( /\\+/g , \" \" )); } /** * Gets a url param by name and attempts to parse a bool value * * @param name The name of the parameter for which we want the boolean value */ public static getUrlParamBoolByName ( name : string ) : boolean { const p = this . getUrlParamByName ( name ); const isFalse = ( p === \"\" || /false|0/i . test ( p )); return ! isFalse ; } /** * Inserts the string s into the string target as the index specified by index * * @param target The string into which we will insert s * @param index The location in target to insert s (zero based) * @param s The string to insert into target at position index */ public static stringInsert ( target : string , index : number , s : string ) : string { if ( index > 0 ) { return target . substring ( 0 , index ) + s + target . substring ( index , target . length ); } return s + target ; }","title":"util"},{"location":"common/docs/util/#pnpcommonutil","text":"This module contains utility methods that you can import individually from the common library. import { getRandomString , } from \"@pnp/core\" ; // use from individual;y imported method console . log ( getRandomString ( 10 ));","title":"@pnp/core/util"},{"location":"common/docs/util/#getctxcallback","text":"Gets a callback function which will maintain context across async calls. import { getCtxCallback } from \"@pnp/core\" ; const contextThis = { myProp : 6 , }; function theFunction() { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this . myProp ; } const callback = getCtxCallback ( contextThis , theFunction ); callback (); // returns 6 // You can also supply additional parameters if needed function theFunction2 ( g : number ) { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this . myProp + g ; } const callback2 = getCtxCallback ( contextThis , theFunction , 4 ); callback2 (); // returns 10 (6 + 4)","title":"getCtxCallback"},{"location":"common/docs/util/#dateadd","text":"Manipulates a date, please see the Stackoverflow discussion from where this method was taken.","title":"dateAdd"},{"location":"common/docs/util/#combine","text":"Combines any number of paths, normalizing the slashes as required import { combine } from \"@pnp/core\" ; // \"https://microsoft.com/something/more\" const paths = combine ( \"https://microsoft.com\" , \"something\" , \"more\" ); // \"also/works/with/relative\" const paths2 = combine ( \"/also/\" , \"/works\" , \"with/\" , \"/relative\\\\\" );","title":"combine"},{"location":"common/docs/util/#getrandomstring","text":"Gets a random string consisting of the number of characters requested. import { getRandomString } from \"@pnp/core\" ; const randomString = getRandomString ( 10 );","title":"getRandomString"},{"location":"common/docs/util/#getguid","text":"Creates a random guid, please see the Stackoverflow discussion from where this method was taken.","title":"getGUID"},{"location":"common/docs/util/#isfunc","text":"Determines if a supplied variable represents a function.","title":"isFunc"},{"location":"common/docs/util/#objectdefinednotnull","text":"Determines if an object is defined and not null.","title":"objectDefinedNotNull"},{"location":"common/docs/util/#isarray","text":"Determines if a supplied variable represents an array.","title":"isArray"},{"location":"common/docs/util/#extend","text":"Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing properties. import { extend } from \"@pnp/core\" ; let obj1 = { prop : 1 , prop2 : 2 , }; const obj2 = { prop : 4 , prop3 : 9 , }; const example1 = extend ( obj1 , obj2 ); // example1 = { prop: 4, prop2: 2, prop3: 9 } const example2 = extend ( obj1 , obj2 , true ); // example2 = { prop: 1, prop2: 2, prop3: 9 }","title":"extend"},{"location":"common/docs/util/#isurlabsolute","text":"Determines if a supplied url is absolute and returns true; otherwise returns false.","title":"isUrlAbsolute"},{"location":"common/docs/util/#stringisnullorempty","text":"Determines if a supplied string is null or empty","title":"stringIsNullOrEmpty"},{"location":"common/docs/util/#removed","text":"Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods below for use in your projects should you require. /** * Loads a stylesheet into the current page * * @param path The url to the stylesheet * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues */ public static loadStylesheet ( path : string , avoidCache : boolean ) : void { if ( avoidCache ) { path += \"?\" + encodeURIComponent (( new Date ()). getTime (). toString ()); } const head = document . getElementsByTagName ( \"head\" ); if ( head . length > 0 ) { const e = document . createElement ( \"link\" ); head [ 0 ]. appendChild ( e ); e . setAttribute ( \"type\" , \"text/css\" ); e . setAttribute ( \"rel\" , \"stylesheet\" ); e . setAttribute ( \"href\" , path ); } } /** * Tests if a url param exists * * @param name The name of the url parameter to check */ public static urlParamExists ( name : string ) : boolean { name = name . replace ( /[\\[]/ , \"\\\\[\" ). replace ( /[\\]]/ , \"\\\\]\" ); const regex = new RegExp ( \"[\\\\?&]\" + name + \"=([^&#]*)\" ); return regex . test ( location . search ); } /** * Gets a url param value by name * * @param name The name of the parameter for which we want the value */ public static getUrlParamByName ( name : string ) : string { name = name . replace ( /[\\[]/ , \"\\\\[\" ). replace ( /[\\]]/ , \"\\\\]\" ); const regex = new RegExp ( \"[\\\\?&]\" + name + \"=([^&#]*)\" ); const results = regex . exec ( location . search ); return results == null ? \"\" : decodeURIComponent ( results [ 1 ]. replace ( /\\+/g , \" \" )); } /** * Gets a url param by name and attempts to parse a bool value * * @param name The name of the parameter for which we want the boolean value */ public static getUrlParamBoolByName ( name : string ) : boolean { const p = this . getUrlParamByName ( name ); const isFalse = ( p === \"\" || /false|0/i . test ( p )); return ! isFalse ; } /** * Inserts the string s into the string target as the index specified by index * * @param target The string into which we will insert s * @param index The location in target to insert s (zero based) * @param s The string to insert into target at position index */ public static stringInsert ( target : string , index : number , s : string ) : string { if ( index > 0 ) { return target . substring ( 0 , index ) + s + target . substring ( index , target . length ); } return s + target ; }","title":"Removed"},{"location":"config-store/docs/","text":"@pnp/config-store \u00b6 This module providers a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/config-store --save See the topics below for usage: configuration providers UML \u00b6 Graphical UML diagram of @pnp/config-store. Right-click the diagram and open in new tab if it is too small.","title":"config-store"},{"location":"config-store/docs/#pnpconfig-store","text":"This module providers a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed.","title":"@pnp/config-store"},{"location":"config-store/docs/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/config-store --save See the topics below for usage: configuration providers","title":"Getting Started"},{"location":"config-store/docs/#uml","text":"Graphical UML diagram of @pnp/config-store. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"config-store/docs/configuration/","text":"@pnp/config-store/configuration \u00b6 The main class exported from the config-store package is Settings. This is the class through which you will load and access your settings via providers . import { Web } from \"@pnp/sp\" ; import { Settings , SPListConfigurationProvider } from \"@pnp/config-store\" ; // create an instance of the settings class, could be static and shared across your application // or built as needed. const settings = new Settings (); // you can add/update a single value using add settings . add ( \"mykey\" , \"myvalue\" ); // you can also add/update a JSON value which will be stringified for you as a shorthand settings . addJSON ( \"mykey2\" , { field : 1 , field2 : 2 , field3 : 3 , }); // and you can apply a plain object of keys/values that will be written as single values // this results in each enumerable property of the supplied object being added to the settings collection settings . apply ({ field : 1 , field2 : 2 , field3 : 3 , }); // and finally you can load values from a configuration provider const w = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const provider = new SPListConfigurationProvider ( w , \"myconfiglistname\" ); // this will load values from the supplied list // by default the key will be from the Title field and the value from a column named Value await settings . load ( provider ); // once we have loaded values we can then read them const value = settings . get ( \"mykey\" ); // or read JSON that will be parsed for you from the store const value2 = settings . getJSON ( \"mykey2\" );","title":"configuration"},{"location":"config-store/docs/configuration/#pnpconfig-storeconfiguration","text":"The main class exported from the config-store package is Settings. This is the class through which you will load and access your settings via providers . import { Web } from \"@pnp/sp\" ; import { Settings , SPListConfigurationProvider } from \"@pnp/config-store\" ; // create an instance of the settings class, could be static and shared across your application // or built as needed. const settings = new Settings (); // you can add/update a single value using add settings . add ( \"mykey\" , \"myvalue\" ); // you can also add/update a JSON value which will be stringified for you as a shorthand settings . addJSON ( \"mykey2\" , { field : 1 , field2 : 2 , field3 : 3 , }); // and you can apply a plain object of keys/values that will be written as single values // this results in each enumerable property of the supplied object being added to the settings collection settings . apply ({ field : 1 , field2 : 2 , field3 : 3 , }); // and finally you can load values from a configuration provider const w = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const provider = new SPListConfigurationProvider ( w , \"myconfiglistname\" ); // this will load values from the supplied list // by default the key will be from the Title field and the value from a column named Value await settings . load ( provider ); // once we have loaded values we can then read them const value = settings . get ( \"mykey\" ); // or read JSON that will be parsed for you from the store const value2 = settings . getJSON ( \"mykey2\" );","title":"@pnp/config-store/configuration"},{"location":"config-store/docs/providers/","text":"@pnp/config-store/providers \u00b6 Currently there is a single provider included in the library, but contributions of additional providers are welcome. SPListConfigurationProvider \u00b6 This provider is based on a SharePoint list and read all of the rows and makes them available as a TypedHash . By default the column names used are Title for key and \"Value\" for value, but you can update these as needed. Additionally the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. import { Web } from \"@pnp/sp\" ; import { Settings , SPListConfigurationProvider } from \"@pnp/config-store\" ; // create a new provider instance const w = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const provider = new SPListConfigurationProvider ( w , \"myconfiglistname\" ); const settings = new Settings (); // load our values from the list await settings . load ( provider ); CachingConfigurationProvider \u00b6 Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a provider and caches the configuration in local or session storage. import { Web } from \"@pnp/sp\" ; import { Settings , SPListConfigurationProvider } from \"@pnp/config-store\" ; // create a new provider instance const w = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const provider = new SPListConfigurationProvider ( w , \"myconfiglistname\" ); // get an instance of the provider wrapped // you can optionally provide a key that will be used in the cache to the asCaching method const wrappedProvider = provider . asCaching (); // use that wrapped provider to populate the settings await settings . load ( wrappedProvider );","title":"providers"},{"location":"config-store/docs/providers/#pnpconfig-storeproviders","text":"Currently there is a single provider included in the library, but contributions of additional providers are welcome.","title":"@pnp/config-store/providers"},{"location":"config-store/docs/providers/#splistconfigurationprovider","text":"This provider is based on a SharePoint list and read all of the rows and makes them available as a TypedHash . By default the column names used are Title for key and \"Value\" for value, but you can update these as needed. Additionally the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. import { Web } from \"@pnp/sp\" ; import { Settings , SPListConfigurationProvider } from \"@pnp/config-store\" ; // create a new provider instance const w = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const provider = new SPListConfigurationProvider ( w , \"myconfiglistname\" ); const settings = new Settings (); // load our values from the list await settings . load ( provider );","title":"SPListConfigurationProvider"},{"location":"config-store/docs/providers/#cachingconfigurationprovider","text":"Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a provider and caches the configuration in local or session storage. import { Web } from \"@pnp/sp\" ; import { Settings , SPListConfigurationProvider } from \"@pnp/config-store\" ; // create a new provider instance const w = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const provider = new SPListConfigurationProvider ( w , \"myconfiglistname\" ); // get an instance of the provider wrapped // you can optionally provide a key that will be used in the cache to the asCaching method const wrappedProvider = provider . asCaching (); // use that wrapped provider to populate the settings await settings . load ( wrappedProvider );","title":"CachingConfigurationProvider"},{"location":"documentation/SPFx-On-Premesis-2016/","text":"Workaround for SPFx TypeScript Version \u00b6 Note this article applies to version 1.4.1 SharePoint Framework projects targetting on-premesis only. When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premesis it installs TypeScript version 2.2.2. Unfortunately this library relies on 2.4.2 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. Open package-lock.json Search for \"typescript\": \"2.2.2\" Replace \"2.2.2\" with \"2.4.2\" Search for the next \"typescript\" occurance and replace the block with: \"typescript\" : { \"version\" : \"2.4.2\" , \"resolved\" : \"https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz\" , \"integrity\" : \"sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=\" , \"dev\" : true } Remove node_modules folder rm -rf node_modules/ Run npm install This can be checked with: npm list typescript +-- @microsoft/sp-build-web@1.1.0 | `-- @microsoft/gulp-core-build-typescript@3.1.1 | +-- @microsoft/api-extractor@2.3.8 | | `-- typescript@2.4.2 | `-- typescript@2.4.2","title":"SPFx On-Premises 2016"},{"location":"documentation/SPFx-On-Premesis-2016/#workaround-for-spfx-typescript-version","text":"Note this article applies to version 1.4.1 SharePoint Framework projects targetting on-premesis only. When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premesis it installs TypeScript version 2.2.2. Unfortunately this library relies on 2.4.2 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. Open package-lock.json Search for \"typescript\": \"2.2.2\" Replace \"2.2.2\" with \"2.4.2\" Search for the next \"typescript\" occurance and replace the block with: \"typescript\" : { \"version\" : \"2.4.2\" , \"resolved\" : \"https://registry.npmjs.org/typescript/-/typescript-2.4.2.tgz\" , \"integrity\" : \"sha1-+DlfhdRZJ2BnyYiqQYN6j4KHCEQ=\" , \"dev\" : true } Remove node_modules folder rm -rf node_modules/ Run npm install This can be checked with: npm list typescript +-- @microsoft/sp-build-web@1.1.0 | `-- @microsoft/gulp-core-build-typescript@3.1.1 | +-- @microsoft/api-extractor@2.3.8 | | `-- typescript@2.4.2 | `-- typescript@2.4.2","title":"Workaround for SPFx TypeScript Version"},{"location":"documentation/beta-versions/","text":"Beta Versions \u00b6 To help folks try out new features early and provide feedback prior to releases we publish beta versions of the packages. Released as a set with matching version numbers, just like when we do a normal release. Generally every Friday a new set of beta libraries will be released. While not ready for production use we encourage you to try out these pre-release packages and provide us feedback. Installing \u00b6 To install the beta packages in your project you use the @beta version number on the packages. This applies to all packages, not just the ones shown in the example below. npm install @pnp/logging@beta @pnp/core@beta @pnp/queryable@beta @pnp/sp@beta --save Please remember that it is possible something may not work in a beta version, so be aware and if you find something please report an issue .","title":"Install Beta Versions"},{"location":"documentation/beta-versions/#beta-versions","text":"To help folks try out new features early and provide feedback prior to releases we publish beta versions of the packages. Released as a set with matching version numbers, just like when we do a normal release. Generally every Friday a new set of beta libraries will be released. While not ready for production use we encourage you to try out these pre-release packages and provide us feedback.","title":"Beta Versions"},{"location":"documentation/beta-versions/#installing","text":"To install the beta packages in your project you use the @beta version number on the packages. This applies to all packages, not just the ones shown in the example below. npm install @pnp/logging@beta @pnp/core@beta @pnp/queryable@beta @pnp/sp@beta --save Please remember that it is possible something may not work in a beta version, so be aware and if you find something please report an issue .","title":"Installing"},{"location":"documentation/debugging/","text":"Debugging \u00b6 Debugging Library Features in Code using Node \u00b6 The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses the launch.json file to build and run the library using ./debug/launch/main.ts as the program entry. You can add any number of files to this directory and they will be ignored by git, however the debug.ts file is not, so please ensure you don't commit any login information. Setup settings.js \u00b6 If you have not already you need to create a settings.js files by copying settings.example.js and renaming it to settings.js. Then update the clientId, clientSecret, and siteUrl fields in the testing section. (See below for guidance on registering a client id and secret) Test your setup \u00b6 If you hit F5 now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly. Create a debug module \u00b6 Using ./debug/launch/example.ts as a reference create a debugging file in the debug folder, let's call it mydebug.ts and add this content: // note we can use the actual package names for our imports import { sp , ListEnsureResult } from \"@pnp/sp\" ; import { Logger , LogLevel , ConsoleListener } from \"@pnp/logging\" ; declare var process : { exit ( code? : number ) : void }; export function MyDebug() { // run some debugging sp . web . lists . ensure ( \"MyFirstList\" ). then (( list : ListEnsureResult ) => { Logger . log ({ data : list.created , message : \"Was list created?\" , level : LogLevel.Verbose }); if ( list . created ) { Logger . log ({ data : list.data , message : \"Raw data from list creation.\" , level : LogLevel.Verbose }); } else { Logger . log ({ data : null , message : \"List already existed!\" , level : LogLevel.Verbose }); } process . exit ( 0 ); }). catch ( e => { Logger . error ( e ); process . exit ( 1 ); }); } Update main.ts to launch your module \u00b6 First comment out the import for the default example and then add the import and function call for yours, the updated main.ts should look like this: // ... // comment out the example // import { Example } from \"./example\"; // Example(); import { MyDebug } from \"./mydebug\" MyDebug (); // ... Debug! \u00b6 Place a break point within the promise resolution in your debug file and hit F5. Your module should be run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios. Next Steps \u00b6 Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally. In Browser Debugging \u00b6 You can also serve files locally to debug in a browser through two methods. The first will serve code using ./debug/serve/main.ts as the entry. Meaning you can easily write code and test it in the browser. The second method allows you to serve a single package (bundled with all dependencies) for in browser testing. Both methods serve the file from https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing. Start the local serve \u00b6 This will serve a package with ./debug/serve/main.ts as the entry. gulp serve Add reference to library \u00b6 Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire. < script src = \"https://localhost:8080/assets/pnp.js\" > < div id = \"pnptestdiv\" > You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but note that any changes included as part of a PR to this file will not be allowed. Serve a specific package \u00b6 For example if you wanted to serve the @pnp/sp package for testing you would use: gulp serve --p sp This will serve a bundle of the sp functionality along with all dependencies and place a global variable named \"pnp.{packagename}\", in this case pnp.sp. This will be true for each package, if you served just the graph package the global would be pnp.graph. This mirrors how the umd modules are built in the distributed npm packages to allow testing with matching packages. Next Steps \u00b6 You can make changes to the library and immediately see them reflected in the browser. All files are watched regardless of which serve method you choose. Register an Add-in \u00b6 Before you can begin debugging you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri, you can use the values shown in the examples Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article. Grant Your Add-In Permissions \u00b6 Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the Add Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note these are very broad permissions to ensure you can test any feature of the library, for production you should tailor the permissions to only those required Configure the project settings file \u00b6 If you have not already, make a copy of settings.example.js and name it settings.js Edit this file to set the values on the testing.sp object to id: \"The client id you created\" secret: \"The client secret you created\" url: \"{site url}\" You can disable web tests at any time by setting enableWebTests to false in settings.js, this can be helpful as they take a few minutes to run","title":"Debugging"},{"location":"documentation/debugging/#debugging","text":"","title":"Debugging"},{"location":"documentation/debugging/#debugging-library-features-in-code-using-node","text":"The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses the launch.json file to build and run the library using ./debug/launch/main.ts as the program entry. You can add any number of files to this directory and they will be ignored by git, however the debug.ts file is not, so please ensure you don't commit any login information.","title":"Debugging Library Features in Code using Node"},{"location":"documentation/debugging/#setup-settingsjs","text":"If you have not already you need to create a settings.js files by copying settings.example.js and renaming it to settings.js. Then update the clientId, clientSecret, and siteUrl fields in the testing section. (See below for guidance on registering a client id and secret)","title":"Setup settings.js"},{"location":"documentation/debugging/#test-your-setup","text":"If you hit F5 now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.","title":"Test your setup"},{"location":"documentation/debugging/#create-a-debug-module","text":"Using ./debug/launch/example.ts as a reference create a debugging file in the debug folder, let's call it mydebug.ts and add this content: // note we can use the actual package names for our imports import { sp , ListEnsureResult } from \"@pnp/sp\" ; import { Logger , LogLevel , ConsoleListener } from \"@pnp/logging\" ; declare var process : { exit ( code? : number ) : void }; export function MyDebug() { // run some debugging sp . web . lists . ensure ( \"MyFirstList\" ). then (( list : ListEnsureResult ) => { Logger . log ({ data : list.created , message : \"Was list created?\" , level : LogLevel.Verbose }); if ( list . created ) { Logger . log ({ data : list.data , message : \"Raw data from list creation.\" , level : LogLevel.Verbose }); } else { Logger . log ({ data : null , message : \"List already existed!\" , level : LogLevel.Verbose }); } process . exit ( 0 ); }). catch ( e => { Logger . error ( e ); process . exit ( 1 ); }); }","title":"Create a debug module"},{"location":"documentation/debugging/#update-maints-to-launch-your-module","text":"First comment out the import for the default example and then add the import and function call for yours, the updated main.ts should look like this: // ... // comment out the example // import { Example } from \"./example\"; // Example(); import { MyDebug } from \"./mydebug\" MyDebug (); // ...","title":"Update main.ts to launch your module"},{"location":"documentation/debugging/#debug","text":"Place a break point within the promise resolution in your debug file and hit F5. Your module should be run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.","title":"Debug!"},{"location":"documentation/debugging/#next-steps","text":"Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally.","title":"Next Steps"},{"location":"documentation/debugging/#in-browser-debugging","text":"You can also serve files locally to debug in a browser through two methods. The first will serve code using ./debug/serve/main.ts as the entry. Meaning you can easily write code and test it in the browser. The second method allows you to serve a single package (bundled with all dependencies) for in browser testing. Both methods serve the file from https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing.","title":"In Browser Debugging"},{"location":"documentation/debugging/#start-the-local-serve","text":"This will serve a package with ./debug/serve/main.ts as the entry. gulp serve","title":"Start the local serve"},{"location":"documentation/debugging/#add-reference-to-library","text":"Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire. < script src = \"https://localhost:8080/assets/pnp.js\" > < div id = \"pnptestdiv\" > You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but note that any changes included as part of a PR to this file will not be allowed.","title":"Add reference to library"},{"location":"documentation/debugging/#serve-a-specific-package","text":"For example if you wanted to serve the @pnp/sp package for testing you would use: gulp serve --p sp This will serve a bundle of the sp functionality along with all dependencies and place a global variable named \"pnp.{packagename}\", in this case pnp.sp. This will be true for each package, if you served just the graph package the global would be pnp.graph. This mirrors how the umd modules are built in the distributed npm packages to allow testing with matching packages.","title":"Serve a specific package"},{"location":"documentation/debugging/#next-steps_1","text":"You can make changes to the library and immediately see them reflected in the browser. All files are watched regardless of which serve method you choose.","title":"Next Steps"},{"location":"documentation/debugging/#register-an-add-in","text":"Before you can begin debugging you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri, you can use the values shown in the examples Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.","title":"Register an Add-in"},{"location":"documentation/debugging/#grant-your-add-in-permissions","text":"Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the Add Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note these are very broad permissions to ensure you can test any feature of the library, for production you should tailor the permissions to only those required","title":"Grant Your Add-In Permissions"},{"location":"documentation/debugging/#configure-the-project-settings-file","text":"If you have not already, make a copy of settings.example.js and name it settings.js Edit this file to set the values on the testing.sp object to id: \"The client id you created\" secret: \"The client secret you created\" url: \"{site url}\" You can disable web tests at any time by setting enableWebTests to false in settings.js, this can be helpful as they take a few minutes to run","title":"Configure the project settings file"},{"location":"documentation/deployment/","text":"Deployment \u00b6 There are two recommended ways to consume the library in a production deployment: bundle the code into your solution (such as with webpack), or reference the code from a CDN. These methods are outlined here but this is not meant to be an exhaustive guide on all the ways to package and deploy solutions. Bundle \u00b6 If you have installed the library via NPM into your application solution bundlers such as webpack can bundle the PnPjs libraries along with your solution. This can make deployment easier, but will increase the size of your application by the size of the included libraries. The PnPjs libraries are setup to support tree shaking which can help with the bundle size. CDN \u00b6 If you have public internet access you can reference the library from cdnjs or unpkg which maintains copies of all versions. This is ideal as you do not need to host the file yourself, and it is easy to update to a newer release by updating the URL in your solution. Below lists all of the library locations within cdnjs, you will need to ensure you have the full url to the file you need, such as: \"https://cdnjs.cloudflare.com/ajax/libs/pnp-common/1.1.1/common.es5.umd.min.js\". To use the libraries with a script tag in a page it is recommended to use the *.es5.umd.min.js versions. This will add a global pnp value with each library added as pnp.{lib name} such as pnp.sp, pnp.common, etc. https://cdnjs.com/libraries/pnp-common https://cdnjs.com/libraries/pnp-config-store https://cdnjs.com/libraries/pnp-graph https://cdnjs.com/libraries/pnp-logging https://cdnjs.com/libraries/pnp-odata https://cdnjs.com/libraries/pnp-pnpjs https://cdnjs.com/libraries/pnp-sp https://cdnjs.com/libraries/pnp-sp-addinhelpers https://cdnjs.com/libraries/pnp-sp-clientsvc https://cdnjs.com/libraries/pnp-sp-taxonomy CDN and SPFx \u00b6 If you are developing in SPFx and install and import the PnPjs libraries the default behavior will be to bundle the library into your solution. You have a couple of choices on how best to work with CDNs and SPFx. Because SPFx doesn't currently respect peer dependencies it is easier to reference the pnpjs rollup package for your project. In this case you would install the package, reference it in your code, and update your config.js file externals as follows: Install \u00b6 npm install @pnp/pnpjs --save In Code \u00b6 import { sp } from \"@pnp/pnpjs\" ; sp . web . lists . getByTitle ( \"BigList\" ). get (). then ( r => { this . domElement . innerHTML += r . Title ; }); config.json \u00b6 \"externals\" : { \"@pnp/pnpjs\" : { \"path\" : \"https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.1.4/pnpjs.es5.umd.bundle.min.js\" , \"globalName\" : \"pnp\" } } , You can still work with the individual packages from a cdn, but you have a bit more work to do. First install the modules you need, update the config with the JSON externals below, and add some blind require statements into your code. These are needed because peer dependencies are not processed by SPFx so you have to \"trigger\" the SPFx manifest creator to include those packages. Note this approach requires using version 1.1.5 (specifically beta 1.1.5-2) or later of the libraries as we had make a few updates to how things are packaged to make this a little easier. Install \u00b6 npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save In Code \u00b6 // blind require statements require ( \"tslib\" ); require ( \"@pnp/logging\" ); require ( \"@pnp/core\" ); require ( \"@pnp/queryable\" ); import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"BigList\" ). get (). then ( r => { this . domElement . innerHTML += r . Title ; }); config.json \u00b6 \"externals\" : { \"@pnp/sp\" : { \"path\" : \"https://unpkg.com/@pnp/sp@1.1.5-2/dist/sp.es5.umd.min.js\" , \"globalName\" : \"pnp.sp\" , \"globalDependencies\" : [ \"@pnp/logging\" , \"@pnp/core\" , \"@pnp/queryable\" , \"tslib\" ] }, \"@pnp/queryable\" : { \"path\" : \"https://unpkg.com/@pnp/queryable@1.1.5-2/dist/odata.es5.umd.min.js\" , \"globalName\" : \"pnp.odata\" , \"globalDependencies\" : [ \"@pnp/core\" , \"@pnp/logging\" , \"tslib\" ] }, \"@pnp/core\" : { \"path\" : \"https://unpkg.com/@pnp/core@1.1.5-2/dist/common.es5.umd.bundle.min.js\" , \"globalName\" : \"pnp.common\" }, \"@pnp/logging\" : { \"path\" : \"https://unpkg.com/@pnp/logging@1.1.5-2/dist/logging.es5.umd.min.js\" , \"globalName\" : \"pnp.logging\" , \"globalDependencies\" : [ \"tslib\" ] }, \"tslib\" : { \"path\" : \"https://cdnjs.cloudflare.com/ajax/libs/tslib/1.9.3/tslib.min.js\" , \"globalName\" : \"tslib\" } } Don't forget to update the version number in the url to match the version you want to use. This will stop the library from being bundled directly into the solution and instead use the copy from the CDN. When a new version of the PnPjs libraries are released and you are ready to update just update this url in your SPFX project's config.js file.","title":"Deployment"},{"location":"documentation/deployment/#deployment","text":"There are two recommended ways to consume the library in a production deployment: bundle the code into your solution (such as with webpack), or reference the code from a CDN. These methods are outlined here but this is not meant to be an exhaustive guide on all the ways to package and deploy solutions.","title":"Deployment"},{"location":"documentation/deployment/#bundle","text":"If you have installed the library via NPM into your application solution bundlers such as webpack can bundle the PnPjs libraries along with your solution. This can make deployment easier, but will increase the size of your application by the size of the included libraries. The PnPjs libraries are setup to support tree shaking which can help with the bundle size.","title":"Bundle"},{"location":"documentation/deployment/#cdn","text":"If you have public internet access you can reference the library from cdnjs or unpkg which maintains copies of all versions. This is ideal as you do not need to host the file yourself, and it is easy to update to a newer release by updating the URL in your solution. Below lists all of the library locations within cdnjs, you will need to ensure you have the full url to the file you need, such as: \"https://cdnjs.cloudflare.com/ajax/libs/pnp-common/1.1.1/common.es5.umd.min.js\". To use the libraries with a script tag in a page it is recommended to use the *.es5.umd.min.js versions. This will add a global pnp value with each library added as pnp.{lib name} such as pnp.sp, pnp.common, etc. https://cdnjs.com/libraries/pnp-common https://cdnjs.com/libraries/pnp-config-store https://cdnjs.com/libraries/pnp-graph https://cdnjs.com/libraries/pnp-logging https://cdnjs.com/libraries/pnp-odata https://cdnjs.com/libraries/pnp-pnpjs https://cdnjs.com/libraries/pnp-sp https://cdnjs.com/libraries/pnp-sp-addinhelpers https://cdnjs.com/libraries/pnp-sp-clientsvc https://cdnjs.com/libraries/pnp-sp-taxonomy","title":"CDN"},{"location":"documentation/deployment/#cdn-and-spfx","text":"If you are developing in SPFx and install and import the PnPjs libraries the default behavior will be to bundle the library into your solution. You have a couple of choices on how best to work with CDNs and SPFx. Because SPFx doesn't currently respect peer dependencies it is easier to reference the pnpjs rollup package for your project. In this case you would install the package, reference it in your code, and update your config.js file externals as follows:","title":"CDN and SPFx"},{"location":"documentation/deployment/#install","text":"npm install @pnp/pnpjs --save","title":"Install"},{"location":"documentation/deployment/#in-code","text":"import { sp } from \"@pnp/pnpjs\" ; sp . web . lists . getByTitle ( \"BigList\" ). get (). then ( r => { this . domElement . innerHTML += r . Title ; });","title":"In Code"},{"location":"documentation/deployment/#configjson","text":"\"externals\" : { \"@pnp/pnpjs\" : { \"path\" : \"https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.1.4/pnpjs.es5.umd.bundle.min.js\" , \"globalName\" : \"pnp\" } } , You can still work with the individual packages from a cdn, but you have a bit more work to do. First install the modules you need, update the config with the JSON externals below, and add some blind require statements into your code. These are needed because peer dependencies are not processed by SPFx so you have to \"trigger\" the SPFx manifest creator to include those packages. Note this approach requires using version 1.1.5 (specifically beta 1.1.5-2) or later of the libraries as we had make a few updates to how things are packaged to make this a little easier.","title":"config.json"},{"location":"documentation/deployment/#install_1","text":"npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save","title":"Install"},{"location":"documentation/deployment/#in-code_1","text":"// blind require statements require ( \"tslib\" ); require ( \"@pnp/logging\" ); require ( \"@pnp/core\" ); require ( \"@pnp/queryable\" ); import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"BigList\" ). get (). then ( r => { this . domElement . innerHTML += r . Title ; });","title":"In Code"},{"location":"documentation/deployment/#configjson_1","text":"\"externals\" : { \"@pnp/sp\" : { \"path\" : \"https://unpkg.com/@pnp/sp@1.1.5-2/dist/sp.es5.umd.min.js\" , \"globalName\" : \"pnp.sp\" , \"globalDependencies\" : [ \"@pnp/logging\" , \"@pnp/core\" , \"@pnp/queryable\" , \"tslib\" ] }, \"@pnp/queryable\" : { \"path\" : \"https://unpkg.com/@pnp/queryable@1.1.5-2/dist/odata.es5.umd.min.js\" , \"globalName\" : \"pnp.odata\" , \"globalDependencies\" : [ \"@pnp/core\" , \"@pnp/logging\" , \"tslib\" ] }, \"@pnp/core\" : { \"path\" : \"https://unpkg.com/@pnp/core@1.1.5-2/dist/common.es5.umd.bundle.min.js\" , \"globalName\" : \"pnp.common\" }, \"@pnp/logging\" : { \"path\" : \"https://unpkg.com/@pnp/logging@1.1.5-2/dist/logging.es5.umd.min.js\" , \"globalName\" : \"pnp.logging\" , \"globalDependencies\" : [ \"tslib\" ] }, \"tslib\" : { \"path\" : \"https://cdnjs.cloudflare.com/ajax/libs/tslib/1.9.3/tslib.min.js\" , \"globalName\" : \"tslib\" } } Don't forget to update the version number in the url to match the version you want to use. This will stop the library from being bundled directly into the solution and instead use the copy from the CDN. When a new version of the PnPjs libraries are released and you are ready to update just update this url in your SPFX project's config.js file.","title":"config.json"},{"location":"documentation/documentation/","text":"Building the Documentation \u00b6 Building the documentation locally can help you visualize change you are making to the docs. What you see locally should be what you see online. Building \u00b6 Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable . When executing the pip module on Windows you can prefix it with python -m . For example: python -m pip install mkdocs-material Install MkDocs pip install mkdocs Install the Material theme pip install mkdocs-material install the mkdocs-markdownextradata-plugin - this is used for the version variable pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) Serve it up mkdocs serve Open a browser to http://127.0.0.1:8000/","title":"Building Docs"},{"location":"documentation/documentation/#building-the-documentation","text":"Building the documentation locally can help you visualize change you are making to the docs. What you see locally should be what you see online.","title":"Building the Documentation"},{"location":"documentation/documentation/#building","text":"Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable . When executing the pip module on Windows you can prefix it with python -m . For example: python -m pip install mkdocs-material Install MkDocs pip install mkdocs Install the Material theme pip install mkdocs-material install the mkdocs-markdownextradata-plugin - this is used for the version variable pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) Serve it up mkdocs serve Open a browser to http://127.0.0.1:8000/","title":"Building"},{"location":"documentation/getting-started-dev/","text":"Contribution Guide \u00b6 Thank you for your interest in contributing to our work. This guide should help you get started, please let us know if you have any questions. Contributor Guidance \u00b6 Target your pull requests to the dev branch Add/Update any relevant docs articles in the relevant package's docs folder related to your changes Include a test for any new functionality and ensure all existing tests are passing by running gulp test Ensure tslint checks pass by typing gulp lint Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :) Setup your development environment \u00b6 These steps will help you get your environment setup for contributing to the core library. Install Visual Studio Code - this is the development environment we will use. It is similar to a light-weight Visual Studio designed for each editing of client file types such as .ts and .js. (Note that if you prefer you can use Visual Studio). Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). On Windows: Install Python v2.7.10 - this is used by some of the plug-ins and build tools inside Node JS - (Python v3.x.x is not supported by those modules). If Visual Studio is not installed on the client in addition to this C++ runtime is required. Please see node-gyp Readme Install a console emulator of your choice, for Windows Cmder is popular. If installing Cmder choosing the full option will allow you to use git for windows. Whatever option you choose we will refer in the rest of the guide to \"console\" as the thing you installed in this step. Install the tslint extension in VS Code: Press Shift + Ctrl + \"p\" to open the command panel Begin typing \"install extension\" and select the command when it appears in view Begin typing \"tslint\" and select the package when it appears in view Restart Code after installation Install the gulp command line globally by typing the following code in your console npm install -g gulp-cli Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool. Once you have the code locally, navigate to the root of the project in your console. Type the following command: npm install - installs all of the npm package dependencies (may take awhile the first time) Copy settings.example.js in the root of your project to settings.js. Edit settings.js to reflect your personal environment (usename, password, siteUrl, etc.). Then you can follow the guidance in the debugging article to get started testing right away!","title":"Getting Started Contributing"},{"location":"documentation/getting-started-dev/#contribution-guide","text":"Thank you for your interest in contributing to our work. This guide should help you get started, please let us know if you have any questions.","title":"Contribution Guide"},{"location":"documentation/getting-started-dev/#contributor-guidance","text":"Target your pull requests to the dev branch Add/Update any relevant docs articles in the relevant package's docs folder related to your changes Include a test for any new functionality and ensure all existing tests are passing by running gulp test Ensure tslint checks pass by typing gulp lint Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :)","title":"Contributor Guidance"},{"location":"documentation/getting-started-dev/#setup-your-development-environment","text":"These steps will help you get your environment setup for contributing to the core library. Install Visual Studio Code - this is the development environment we will use. It is similar to a light-weight Visual Studio designed for each editing of client file types such as .ts and .js. (Note that if you prefer you can use Visual Studio). Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). On Windows: Install Python v2.7.10 - this is used by some of the plug-ins and build tools inside Node JS - (Python v3.x.x is not supported by those modules). If Visual Studio is not installed on the client in addition to this C++ runtime is required. Please see node-gyp Readme Install a console emulator of your choice, for Windows Cmder is popular. If installing Cmder choosing the full option will allow you to use git for windows. Whatever option you choose we will refer in the rest of the guide to \"console\" as the thing you installed in this step. Install the tslint extension in VS Code: Press Shift + Ctrl + \"p\" to open the command panel Begin typing \"install extension\" and select the command when it appears in view Begin typing \"tslint\" and select the package when it appears in view Restart Code after installation Install the gulp command line globally by typing the following code in your console npm install -g gulp-cli Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool. Once you have the code locally, navigate to the root of the project in your console. Type the following command: npm install - installs all of the npm package dependencies (may take awhile the first time) Copy settings.example.js in the root of your project to settings.js. Edit settings.js to reflect your personal environment (usename, password, siteUrl, etc.). Then you can follow the guidance in the debugging article to get started testing right away!","title":"Setup your development environment"},{"location":"documentation/getting-started/","text":"Getting Started \u00b6 These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. If you need to support older browsers please review the article on polyfills for required functionality. Install \u00b6 First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project. npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/graph --save Next we can import and use the functionality within our application. The below is a very simple example, please see the individual package documentation for more details. import { getRandomString } from \"@pnp/core\" ; ( function () { // get and log a random string console . log ( getRandomString ( 20 )); })() Getting Started with SharePoint Framework \u00b6 The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 on-premises please read this note on a workaround for the included TypeScript version. If you are targetting SharePoint online you do not need to take any additional steps. Establish Context \u00b6 Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the spfx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. The setup is always done in the onInit method to ensure it runs before your other lifecycle code. You can also set any other settings at this time. Using @pnp/core setup \u00b6 import { setup as pnpSetup } from \"@pnp/core\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present pnpSetup ({ spfxContext : this.context }); }); } // ... Using @pnp/sp setup \u00b6 import { sp } from \"@pnp/sp\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present sp . setup ({ spfxContext : this.context }); }); } // ... Using @pnp/graph setup \u00b6 import { graph } from \"@pnp/graph\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present graph . setup ({ spfxContext : this.context }); }); } // ... Establish Context with an SPFx Service \u00b6 Because you do not have access to the full context object within a service you need to setup things slightly differently. This works for the sp library, but not the graph library as we don't have access to the AAD token provider from the full context. import { ServiceKey , ServiceScope } from \"@microsoft/sp-core-library\" ; import { PageContext } from \"@microsoft/sp-page-context\" ; import { AadTokenProviderFactory } from \"@microsoft/sp-http\" ; import { sp } from \"@pnp/sp\" ; export interface ISampleService { getLists () : Promise < any [] > ; } export class SampleService { public static readonly serviceKey : ServiceKey < ISampleService > = ServiceKey . create < ISampleService > ( 'SPFx:SampleService' , SampleService ); constructor ( serviceScope : ServiceScope ) { serviceScope . whenFinished (() => { const pageContext = serviceScope . consume ( PageContext . serviceKey ); const tokenProviderFactory = serviceScope . consume ( AadTokenProviderFactory . serviceKey ); // we need to \"spoof\" the context object with the parts we need for PnPjs sp . setup ({ spfxContext : { aadTokenProviderFactory : tokenProviderFactory , pageContext : pageContext , } }); // This approach also works if you do not require AAD tokens // you don't need to do both // sp.setup({ // sp : { // baseUrl : pageContext.web.absoluteUrl // } // }); }); } public getLists () : Promise < any [] > { return sp . web . lists . get (); } } Connect to SharePoint from Node \u00b6 Because peer dependencies are not installed automatically you will need to list out each package to install. Don't worry if you forget one you will get a message on the command line that a peer dependency is missing. Let's for example look at installing the required libraries to connect to SharePoint from nodejs. You can see ./debug/launch/sp.ts for a live example. npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: import { sp } from \"@pnp/sp\" ; import { SPFetchClient } from \"@pnp/nodejs\" ; Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. // configure your node options (only once in your application) sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" ); }, }, }); // make a call to SharePoint and log it in the console sp . web . select ( \"Title\" , \"Description\" ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }); Connect to Microsoft Graph From Node \u00b6 Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example. npm i @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs Now we need to import what we'll need to call graph import { graph } from \"@pnp/graph\" ; import { AdalFetchClient } from \"@pnp/nodejs\" ; Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. graph . setup ({ graph : { fetchClientFactory : () => { return new AdalFetchClient ( \"{mytenant}.onmicrosoft.com\" , \"{application id}\" , \"{application secret}\" ); }, }, }); // make a call to Graph and get all the groups graph . v1 . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }); Getting Started outside SharePoint Framework \u00b6 In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options. Set baseUrl through setup: \u00b6 Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when working against unpatched versions of SharePoint 2013 as discussed here . This is optional for 2016 or SharePoint Online. import { sp } from \"@pnp/sp\" ; sp . setup ({ sp : { headers : { Accept : \"application/json;odata=verbose\" , }, baseUrl : \"{Absolute SharePoint Web URL}\" }, }); const w = await sp . web . get (); Create Web instances directly \u00b6 Using this method you create the web directly with the url you want to use as the base. import { Web } from \"@pnp/sp\" ; const web = new Web ( \"{Absolute SharePoint Web URL}\" ); const w = await web . get ();","title":"Getting Started"},{"location":"documentation/getting-started/#getting-started","text":"These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. If you need to support older browsers please review the article on polyfills for required functionality.","title":"Getting Started"},{"location":"documentation/getting-started/#install","text":"First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project. npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/graph --save Next we can import and use the functionality within our application. The below is a very simple example, please see the individual package documentation for more details. import { getRandomString } from \"@pnp/core\" ; ( function () { // get and log a random string console . log ( getRandomString ( 20 )); })()","title":"Install"},{"location":"documentation/getting-started/#getting-started-with-sharepoint-framework","text":"The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 on-premises please read this note on a workaround for the included TypeScript version. If you are targetting SharePoint online you do not need to take any additional steps.","title":"Getting Started with SharePoint Framework"},{"location":"documentation/getting-started/#establish-context","text":"Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the spfx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. The setup is always done in the onInit method to ensure it runs before your other lifecycle code. You can also set any other settings at this time.","title":"Establish Context"},{"location":"documentation/getting-started/#using-pnpcommon-setup","text":"import { setup as pnpSetup } from \"@pnp/core\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present pnpSetup ({ spfxContext : this.context }); }); } // ...","title":"Using @pnp/core setup"},{"location":"documentation/getting-started/#using-pnpsp-setup","text":"import { sp } from \"@pnp/sp\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present sp . setup ({ spfxContext : this.context }); }); } // ...","title":"Using @pnp/sp setup"},{"location":"documentation/getting-started/#using-pnpgraph-setup","text":"import { graph } from \"@pnp/graph\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present graph . setup ({ spfxContext : this.context }); }); } // ...","title":"Using @pnp/graph setup"},{"location":"documentation/getting-started/#establish-context-with-an-spfx-service","text":"Because you do not have access to the full context object within a service you need to setup things slightly differently. This works for the sp library, but not the graph library as we don't have access to the AAD token provider from the full context. import { ServiceKey , ServiceScope } from \"@microsoft/sp-core-library\" ; import { PageContext } from \"@microsoft/sp-page-context\" ; import { AadTokenProviderFactory } from \"@microsoft/sp-http\" ; import { sp } from \"@pnp/sp\" ; export interface ISampleService { getLists () : Promise < any [] > ; } export class SampleService { public static readonly serviceKey : ServiceKey < ISampleService > = ServiceKey . create < ISampleService > ( 'SPFx:SampleService' , SampleService ); constructor ( serviceScope : ServiceScope ) { serviceScope . whenFinished (() => { const pageContext = serviceScope . consume ( PageContext . serviceKey ); const tokenProviderFactory = serviceScope . consume ( AadTokenProviderFactory . serviceKey ); // we need to \"spoof\" the context object with the parts we need for PnPjs sp . setup ({ spfxContext : { aadTokenProviderFactory : tokenProviderFactory , pageContext : pageContext , } }); // This approach also works if you do not require AAD tokens // you don't need to do both // sp.setup({ // sp : { // baseUrl : pageContext.web.absoluteUrl // } // }); }); } public getLists () : Promise < any [] > { return sp . web . lists . get (); } }","title":"Establish Context with an SPFx Service"},{"location":"documentation/getting-started/#connect-to-sharepoint-from-node","text":"Because peer dependencies are not installed automatically you will need to list out each package to install. Don't worry if you forget one you will get a message on the command line that a peer dependency is missing. Let's for example look at installing the required libraries to connect to SharePoint from nodejs. You can see ./debug/launch/sp.ts for a live example. npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: import { sp } from \"@pnp/sp\" ; import { SPFetchClient } from \"@pnp/nodejs\" ; Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. // configure your node options (only once in your application) sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" ); }, }, }); // make a call to SharePoint and log it in the console sp . web . select ( \"Title\" , \"Description\" ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"Connect to SharePoint from Node"},{"location":"documentation/getting-started/#connect-to-microsoft-graph-from-node","text":"Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example. npm i @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs Now we need to import what we'll need to call graph import { graph } from \"@pnp/graph\" ; import { AdalFetchClient } from \"@pnp/nodejs\" ; Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. graph . setup ({ graph : { fetchClientFactory : () => { return new AdalFetchClient ( \"{mytenant}.onmicrosoft.com\" , \"{application id}\" , \"{application secret}\" ); }, }, }); // make a call to Graph and get all the groups graph . v1 . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); });","title":"Connect to Microsoft Graph From Node"},{"location":"documentation/getting-started/#getting-started-outside-sharepoint-framework","text":"In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options.","title":"Getting Started outside SharePoint Framework"},{"location":"documentation/getting-started/#set-baseurl-through-setup","text":"Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when working against unpatched versions of SharePoint 2013 as discussed here . This is optional for 2016 or SharePoint Online. import { sp } from \"@pnp/sp\" ; sp . setup ({ sp : { headers : { Accept : \"application/json;odata=verbose\" , }, baseUrl : \"{Absolute SharePoint Web URL}\" }, }); const w = await sp . web . get ();","title":"Set baseUrl through setup:"},{"location":"documentation/getting-started/#create-web-instances-directly","text":"Using this method you create the web directly with the url you want to use as the base. import { Web } from \"@pnp/sp\" ; const web = new Web ( \"{Absolute SharePoint Web URL}\" ); const w = await web . get ();","title":"Create Web instances directly"},{"location":"documentation/gulp-commands/","text":"Gulp Commands \u00b6 This library uses Gulp to orchestrate various tasks. The tasks described below are available for your use. Please review the getting started for development to ensure you've setup your environment correctly. The source for the gulp commands can be found in the tools\\gulptasks folder at the root of the project. Basics \u00b6 All gulp commands are run on the command line in the fashion shown below. gulp [optional pararms] build \u00b6 The build command transpiles the solution from TypeScript into JavaScript using our custom build system . It is controlled by the pnp-build.js file at the project root. Build all of the packages \u00b6 gulp build Building individual packages \u00b6 Note when building a single package none of the dependencies are currently built, so you need to specify in order those packages to build which are dependencies. # fails gulp build --p sp # works as all the dependencies are built in order gulp build --p logging,common,odata,sp You can also build the packages and then not clean using the nc flag. So for example if you are working on the sp package you can build all the packages once, then use the \"nc\" flag to leave those that aren't changing. # run once gulp build --p logging,common,odata,sp # run on subsequent builds gulp build --p sp --nc clean \u00b6 The clean command removes all of the generated folders from the project and is generally used automatically before other commands to ensure there is a clean workspace. gulp clean To clean the build folder. This build folder is no longer included in automatic cleaning after the move to use the TypeScript project references feature that compares previous output and doesn't rebuild unchanged files. This command will erase the entire build folder ensuring you can conduct a clean build/test/etc. gulp clean-build lint \u00b6 Runs the project linting based on the tslint.json rules defined at the project root. This should be done before any PR submissions as linting failures will block merging. gulp lint package \u00b6 Used to create the packages in the ./dist folder as they would exist for a release. gulp package Packaging individual packages \u00b6 You can also package individual packages, but as with build you must also package any dependencies at the same time. gulp package --p logging,common,odata,sp publish \u00b6 This command is only for use by package authors to publish a version to npm and is not for developer use. serve \u00b6 The serve command allows you to serve either code from the ./debug/serve folder OR an individual package for testing in the browser. The file will always be served as https://localhost:8080/assets/pnp.js so can create a static page in your tenant for easy testing of a variety of scenarios. NOTE that in most browsers this file will be flagged as unsafe so you will need to trust it for it to execute on the page. debug serve \u00b6 When running the command with no parameters you will generate a package with the entry being based on the tsconfig.json file in ./debug/serve. By default this will use serve.ts. This allows you to write any code you want to test to easily run it in the browser with all changes being watched and triggering a rebuild. gulp serve package serve \u00b6 If instead you want to test how a particular package will work in the browser you can serve just that package. In this case you do not need to specify the dependencies and specifying multiple packages will throw an error. Packages will be injected into the global namespace on a variable named pnp. gulp serve --p sp test \u00b6 Runs the tests specified in each package's tests folder gulp test Verbose \u00b6 The test command will switch to the \"spec\" mocha reporter if you supply the verbose flag. Doing so will list out each test's description and sucess instead of the \"dot\" used by default. This flag works with all other test options. gulp test --verbose Test individual packages \u00b6 You can test individual packages as needed, and there is no need to include dependencies in this case # test the logging and sp packages gulp test --p logging,sp If you are working on a specific set of tests for a single module you can also use the single or s parameter to select just a single module of tests. You specify the filename without the \".test.ts\" suffix. It must be within the specified package and this option can only be used with a single package for --p # will test only the client-side pages module within the sp package gulp test --p sp --s clientsidepages If you want you can test within the same site and avoid creating a new one, though for some tests this might cause conflicts. This flag can be helpful if you are rapidly testing things with no conflict as you can avoid creating a site each time. Works with both of the above options --p and --s as well as individually. The url must be absolute. #testing using the specified site. gulp test --site https://{tenant}.sharepoint.com/sites/testing # with other options gulp test --p logging,sp --site https://{tenant}.sharepoint.com/sites/testing gulp test --p sp --s clientsidepages --site https://{tenant}.sharepoint.com/sites/testing","title":"Gulp Commands"},{"location":"documentation/gulp-commands/#gulp-commands","text":"This library uses Gulp to orchestrate various tasks. The tasks described below are available for your use. Please review the getting started for development to ensure you've setup your environment correctly. The source for the gulp commands can be found in the tools\\gulptasks folder at the root of the project.","title":"Gulp Commands"},{"location":"documentation/gulp-commands/#basics","text":"All gulp commands are run on the command line in the fashion shown below. gulp [optional pararms]","title":"Basics"},{"location":"documentation/gulp-commands/#build","text":"The build command transpiles the solution from TypeScript into JavaScript using our custom build system . It is controlled by the pnp-build.js file at the project root.","title":"build"},{"location":"documentation/gulp-commands/#build-all-of-the-packages","text":"gulp build","title":"Build all of the packages"},{"location":"documentation/gulp-commands/#building-individual-packages","text":"Note when building a single package none of the dependencies are currently built, so you need to specify in order those packages to build which are dependencies. # fails gulp build --p sp # works as all the dependencies are built in order gulp build --p logging,common,odata,sp You can also build the packages and then not clean using the nc flag. So for example if you are working on the sp package you can build all the packages once, then use the \"nc\" flag to leave those that aren't changing. # run once gulp build --p logging,common,odata,sp # run on subsequent builds gulp build --p sp --nc","title":"Building individual packages"},{"location":"documentation/gulp-commands/#clean","text":"The clean command removes all of the generated folders from the project and is generally used automatically before other commands to ensure there is a clean workspace. gulp clean To clean the build folder. This build folder is no longer included in automatic cleaning after the move to use the TypeScript project references feature that compares previous output and doesn't rebuild unchanged files. This command will erase the entire build folder ensuring you can conduct a clean build/test/etc. gulp clean-build","title":"clean"},{"location":"documentation/gulp-commands/#lint","text":"Runs the project linting based on the tslint.json rules defined at the project root. This should be done before any PR submissions as linting failures will block merging. gulp lint","title":"lint"},{"location":"documentation/gulp-commands/#package","text":"Used to create the packages in the ./dist folder as they would exist for a release. gulp package","title":"package"},{"location":"documentation/gulp-commands/#packaging-individual-packages","text":"You can also package individual packages, but as with build you must also package any dependencies at the same time. gulp package --p logging,common,odata,sp","title":"Packaging individual packages"},{"location":"documentation/gulp-commands/#publish","text":"This command is only for use by package authors to publish a version to npm and is not for developer use.","title":"publish"},{"location":"documentation/gulp-commands/#serve","text":"The serve command allows you to serve either code from the ./debug/serve folder OR an individual package for testing in the browser. The file will always be served as https://localhost:8080/assets/pnp.js so can create a static page in your tenant for easy testing of a variety of scenarios. NOTE that in most browsers this file will be flagged as unsafe so you will need to trust it for it to execute on the page.","title":"serve"},{"location":"documentation/gulp-commands/#debug-serve","text":"When running the command with no parameters you will generate a package with the entry being based on the tsconfig.json file in ./debug/serve. By default this will use serve.ts. This allows you to write any code you want to test to easily run it in the browser with all changes being watched and triggering a rebuild. gulp serve","title":"debug serve"},{"location":"documentation/gulp-commands/#package-serve","text":"If instead you want to test how a particular package will work in the browser you can serve just that package. In this case you do not need to specify the dependencies and specifying multiple packages will throw an error. Packages will be injected into the global namespace on a variable named pnp. gulp serve --p sp","title":"package serve"},{"location":"documentation/gulp-commands/#test","text":"Runs the tests specified in each package's tests folder gulp test","title":"test"},{"location":"documentation/gulp-commands/#verbose","text":"The test command will switch to the \"spec\" mocha reporter if you supply the verbose flag. Doing so will list out each test's description and sucess instead of the \"dot\" used by default. This flag works with all other test options. gulp test --verbose","title":"Verbose"},{"location":"documentation/gulp-commands/#test-individual-packages","text":"You can test individual packages as needed, and there is no need to include dependencies in this case # test the logging and sp packages gulp test --p logging,sp If you are working on a specific set of tests for a single module you can also use the single or s parameter to select just a single module of tests. You specify the filename without the \".test.ts\" suffix. It must be within the specified package and this option can only be used with a single package for --p # will test only the client-side pages module within the sp package gulp test --p sp --s clientsidepages If you want you can test within the same site and avoid creating a new one, though for some tests this might cause conflicts. This flag can be helpful if you are rapidly testing things with no conflict as you can avoid creating a site each time. Works with both of the above options --p and --s as well as individually. The url must be absolute. #testing using the specified site. gulp test --site https://{tenant}.sharepoint.com/sites/testing # with other options gulp test --p logging,sp --site https://{tenant}.sharepoint.com/sites/testing gulp test --p sp --s clientsidepages --site https://{tenant}.sharepoint.com/sites/testing","title":"Test individual packages"},{"location":"documentation/package-structure/","text":"Package Structure \u00b6 Each of the packages is published with the same structure, so this article applies to all of the packages. We will use @pnp/core as an example for discussion. Folders \u00b6 In addition to the files in the root each package has three folders dist, docs, and src. Root Files \u00b6 These files are found at the root of each package. File Description index.d.ts Referenced in package.json typings property and provides the TypeScript type information for consumers LICENSE Package license package.json npm package definition readme.md Basic readme referencing the docs site Dist \u00b6 The dist folder contains the transpiled files bundled in various ways. You can choose the best file for your usage as needed. Below the {package} will be replaced with the name of the package - in our examples case this would be \"common\" making the file name \"{package}.es5.js\" = \"common.es5.js\". All of the *.map files are the debug mapping files related to the .js file of the same name. File Description {package}.es5.js Library packaged in es5 format not wrapped as a module {package}.es5.umd.bundle.js The library bundled with all dependencies into a single UMD module. Global variable will be \"pnp.{package}\". Referenced in the main property of package.json {package}.es5.umd.bundle.min.js Minified version of the bundled umd module {package}.es5.umd.js The library in es5 bundled as a UMD modules with no included dependencies. They are designed to work with the other *.es5.umd.js files. Referenced in the module property of package.json {package}.es5.umd.min.js Minified version of the es5 umd module {package}.js es6 format file of the library. Referenced by es2015 property of package.json Docs \u00b6 This folder contains markdown documentation for the library. All packages will include an index.md which serves as the root of the docs. These files are also used to build the public site . To edit these files they can be found in the packages/{package}/docs folder. Src \u00b6 Contains the TypeScript definition files refrenced by the index.d.ts in the package root. These files serve to provide typing information about the library to consumers who can process typing information.","title":"Package Structure"},{"location":"documentation/package-structure/#package-structure","text":"Each of the packages is published with the same structure, so this article applies to all of the packages. We will use @pnp/core as an example for discussion.","title":"Package Structure"},{"location":"documentation/package-structure/#folders","text":"In addition to the files in the root each package has three folders dist, docs, and src.","title":"Folders"},{"location":"documentation/package-structure/#root-files","text":"These files are found at the root of each package. File Description index.d.ts Referenced in package.json typings property and provides the TypeScript type information for consumers LICENSE Package license package.json npm package definition readme.md Basic readme referencing the docs site","title":"Root Files"},{"location":"documentation/package-structure/#dist","text":"The dist folder contains the transpiled files bundled in various ways. You can choose the best file for your usage as needed. Below the {package} will be replaced with the name of the package - in our examples case this would be \"common\" making the file name \"{package}.es5.js\" = \"common.es5.js\". All of the *.map files are the debug mapping files related to the .js file of the same name. File Description {package}.es5.js Library packaged in es5 format not wrapped as a module {package}.es5.umd.bundle.js The library bundled with all dependencies into a single UMD module. Global variable will be \"pnp.{package}\". Referenced in the main property of package.json {package}.es5.umd.bundle.min.js Minified version of the bundled umd module {package}.es5.umd.js The library in es5 bundled as a UMD modules with no included dependencies. They are designed to work with the other *.es5.umd.js files. Referenced in the module property of package.json {package}.es5.umd.min.js Minified version of the es5 umd module {package}.js es6 format file of the library. Referenced by es2015 property of package.json","title":"Dist"},{"location":"documentation/package-structure/#docs","text":"This folder contains markdown documentation for the library. All packages will include an index.md which serves as the root of the docs. These files are also used to build the public site . To edit these files they can be found in the packages/{package}/docs folder.","title":"Docs"},{"location":"documentation/package-structure/#src","text":"Contains the TypeScript definition files refrenced by the index.d.ts in the package root. These files serve to provide typing information about the library to consumers who can process typing information.","title":"Src"},{"location":"documentation/packages/","text":"The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is ****. @pnp/ common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes pnpjs Rollup library of core functionality (mimics sp-pnp-js) sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins sp-clientsvc Provides base classes for working with the legacy SharePoint sp-taxonomy Provides a fluent api for working with SharePoint Managed Metadata","title":"Packages"},{"location":"documentation/polyfill/","text":"Polyfills \u00b6 These libraries may make use of some features not found in older browsers, mainly fetch, Map, and Proxy. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. There are several ways to include this missing functionality. IE 11 Polyfill package \u00b6 We created a package you can use to include the needed functionality without having to determine what polyfills are required. Also, this package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you need to support IE 11. Install \u00b6 npm install --save @pnp/polyfill-ie11 Use \u00b6 import \"@pnp/polyfill-ie11\" ; import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"BigList\" ). items . filter ( `ID gt 6000` ). get (). then ( r => { this . domElement . innerHTML += r . map ( l => ` ${ l . Title }
` ); }); SearchQueryBuilder \u00b6 Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version for IE 11 as shown below. import \"@pnp/polyfill-ie11\" ; import { SearchQueryBuilder } from \"@pnp/polyfill-ie11/dist/searchquerybuilder\" ; import { sp , ISearchQueryBuilder } from \"@pnp/sp\" ; // works in IE11 and other browsers const builder : ISearchQueryBuilder = SearchQueryBuilder (). text ( \"test\" ); sp . search ( builder ). then ( r => { this . domElement . innerHTML = JSON . stringify ( r ); }); Polyfill Service \u00b6 If acceptable to your design and security requirements you can use a service to provide missing functionality. This loads scripts from a service outside of your and our control, so please ensure you understand any associated risks. To use this option you need to wrap the code in a function, here called \"stuffisloaded\". Then you need to add another script tag as shown below that will load what you need from the polyfill service. Note the parameter \"callback\" takes our function name. < script src = \"https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.2.1/pnpjs.es5.umd.bundle.min.js\" type = \"text/javascript\" > < script > // this function will be executed once the polyfill is loaded. function stuffisloaded () { pnp . sp . web . select ( \"Title\" ). get () . then ( function ( data ){ document . getElementById ( \"main\" ). innerText = data . Title ; }) . catch ( function ( err ){ document . getElementById ( \"main\" ). innerText = err ; }); } < script src = \"https://cdn.polyfill.io/v2/polyfill.min.js?callback=stuffisloaded&features=es6,fetch,Map&flags=always,gated\" > Module Loader \u00b6 If you are using a module loader you need to load the following two files as well. You can do this form a CDN or your style library. Download the es6-promises polyfill from https://github.com/stefanpenner/es6-promise and upload it to your style library. Download the fetch polyfill from https://github.com/github/fetch and upload it to your style library. Download the corejs polyfill from https://github.com/zloirock/core-js and upload it to your style library. Update your module loader to set these files as dependencies before the pnp library is opened. One issue you still may see is that you get errors that certain libraries are undefined when you try to run your code. This is because your code is running before these libraries are loaded. You need to ensure that all dependencies are loaded before making use of the pnp libraries.","title":"Polyfills"},{"location":"documentation/polyfill/#polyfills","text":"These libraries may make use of some features not found in older browsers, mainly fetch, Map, and Proxy. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. There are several ways to include this missing functionality.","title":"Polyfills"},{"location":"documentation/polyfill/#ie-11-polyfill-package","text":"We created a package you can use to include the needed functionality without having to determine what polyfills are required. Also, this package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you need to support IE 11.","title":"IE 11 Polyfill package"},{"location":"documentation/polyfill/#install","text":"npm install --save @pnp/polyfill-ie11","title":"Install"},{"location":"documentation/polyfill/#use","text":"import \"@pnp/polyfill-ie11\" ; import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"BigList\" ). items . filter ( `ID gt 6000` ). get (). then ( r => { this . domElement . innerHTML += r . map ( l => ` ${ l . Title }
` ); });","title":"Use"},{"location":"documentation/polyfill/#searchquerybuilder","text":"Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version for IE 11 as shown below. import \"@pnp/polyfill-ie11\" ; import { SearchQueryBuilder } from \"@pnp/polyfill-ie11/dist/searchquerybuilder\" ; import { sp , ISearchQueryBuilder } from \"@pnp/sp\" ; // works in IE11 and other browsers const builder : ISearchQueryBuilder = SearchQueryBuilder (). text ( \"test\" ); sp . search ( builder ). then ( r => { this . domElement . innerHTML = JSON . stringify ( r ); });","title":"SearchQueryBuilder"},{"location":"documentation/polyfill/#polyfill-service","text":"If acceptable to your design and security requirements you can use a service to provide missing functionality. This loads scripts from a service outside of your and our control, so please ensure you understand any associated risks. To use this option you need to wrap the code in a function, here called \"stuffisloaded\". Then you need to add another script tag as shown below that will load what you need from the polyfill service. Note the parameter \"callback\" takes our function name. < script src = \"https://cdnjs.cloudflare.com/ajax/libs/pnp-pnpjs/1.2.1/pnpjs.es5.umd.bundle.min.js\" type = \"text/javascript\" > < script > // this function will be executed once the polyfill is loaded. function stuffisloaded () { pnp . sp . web . select ( \"Title\" ). get () . then ( function ( data ){ document . getElementById ( \"main\" ). innerText = data . Title ; }) . catch ( function ( err ){ document . getElementById ( \"main\" ). innerText = err ; }); } < script src = \"https://cdn.polyfill.io/v2/polyfill.min.js?callback=stuffisloaded&features=es6,fetch,Map&flags=always,gated\" >","title":"Polyfill Service"},{"location":"documentation/polyfill/#module-loader","text":"If you are using a module loader you need to load the following two files as well. You can do this form a CDN or your style library. Download the es6-promises polyfill from https://github.com/stefanpenner/es6-promise and upload it to your style library. Download the fetch polyfill from https://github.com/github/fetch and upload it to your style library. Download the corejs polyfill from https://github.com/zloirock/core-js and upload it to your style library. Update your module loader to set these files as dependencies before the pnp library is opened. One issue you still may see is that you get errors that certain libraries are undefined when you try to run your code. This is because your code is running before these libraries are loaded. You need to ensure that all dependencies are loaded before making use of the pnp libraries.","title":"Module Loader"},{"location":"documentation/transition-guide/","text":"Transition Guide \u00b6 These libraries are based on the sp-pnp-js library and our goal was to make transition as easy as possible. The most obvious difference is the splitting of the library into multiple packages. We have however created a rollup library to help folks make the move - though our recommendation is to switch to the separate packages. This article outlines transitioning your existing projects from sp-pnp-js to the new libraries, please provide feedback on how we can improve out guidance. Installing @pnp libraries \u00b6 With the separation of the packages we needed a way to indicate how they are related, while making things easy for folks to track and update and we have used peer dependencies between the packages to do this. With each release we will release all packages so that the version numbers move in lock-step, making it easy to ensure you are working with compatible versions. One thing to keep in mind with peer dependencies is that they are not automatically installed. The advantage is you will only have one copy of each library in your project. Installing peer dependencies is easy, you can specify each of the packages in a single line, here we are installing everything required to use the @pnp/sp package. npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp If you do not install all of the peer dependencies you will get a message specifying which ones are missing along with the version expected. Import Simplification \u00b6 With the separation of packages we have also simplified the imports, and allowed you more control over what you are importing. Compare these two examples showing the same set of imports, but one is done via sp-pnp-js and the other using the @pnp libraries. From sp-pnp-js \u00b6 import pnp , { Web , Util , Logger , FunctionListener , LogLevel , } from \"sp-pnp-js\" ; From @pnp libraries \u00b6 import { Logger , LogLevel , FunctionListener } from \"@pnp/logging\" ; import * as Util from \"@pnp/core\" ; import { sp , Web } from \"@pnp/sp\" ; In the above example the \"sp\" import replaces \"pnp\" and is the root of your method chains. Once we have updated our imports we have a few small code changes to make, depending on how you have used the library in your applications. Watch this short video discussing the most common updates: Updated settings file format \u00b6 If you are doing local debugging or testing you have likely created a settings.js from the supplied settings.example.js. Please note the format of that file has changed, the new format is shown below. var settings = { spsave : { username : \"develina.devsson@mydevtenant.onmicrosoft.com\" , password : \"pass@word1\" , siteUrl : \"https://mydevtenant.sharepoint.com/\" }, testing : { enableWebTests : true , sp : { id : \"{ client id }\" , secret : \"{ client secret }\" , url : \"{ site collection url }\" , notificationUrl : \"{ notification url }\" , }, graph : { tenant : \"{tenant.onmicrosoft.com}\" , id : \"{your app id}\" , secret : \"{your secret}\" }, } } HttpClient Renamed \u00b6 If you used HttpClient from sp-pnp-js it was renamed to SPHttpClient. A transition to @pnp/sp assumes replacement of: import { HttpClient } from 'sp-pnp-js' ; to the following import statement: import { SPHttpClient } from '@pnp/sp' ;","title":"Transition Guide"},{"location":"documentation/transition-guide/#transition-guide","text":"These libraries are based on the sp-pnp-js library and our goal was to make transition as easy as possible. The most obvious difference is the splitting of the library into multiple packages. We have however created a rollup library to help folks make the move - though our recommendation is to switch to the separate packages. This article outlines transitioning your existing projects from sp-pnp-js to the new libraries, please provide feedback on how we can improve out guidance.","title":"Transition Guide"},{"location":"documentation/transition-guide/#installing-pnp-libraries","text":"With the separation of the packages we needed a way to indicate how they are related, while making things easy for folks to track and update and we have used peer dependencies between the packages to do this. With each release we will release all packages so that the version numbers move in lock-step, making it easy to ensure you are working with compatible versions. One thing to keep in mind with peer dependencies is that they are not automatically installed. The advantage is you will only have one copy of each library in your project. Installing peer dependencies is easy, you can specify each of the packages in a single line, here we are installing everything required to use the @pnp/sp package. npm i @pnp/logging @pnp/core @pnp/queryable @pnp/sp If you do not install all of the peer dependencies you will get a message specifying which ones are missing along with the version expected.","title":"Installing @pnp libraries"},{"location":"documentation/transition-guide/#import-simplification","text":"With the separation of packages we have also simplified the imports, and allowed you more control over what you are importing. Compare these two examples showing the same set of imports, but one is done via sp-pnp-js and the other using the @pnp libraries.","title":"Import Simplification"},{"location":"documentation/transition-guide/#from-sp-pnp-js","text":"import pnp , { Web , Util , Logger , FunctionListener , LogLevel , } from \"sp-pnp-js\" ;","title":"From sp-pnp-js"},{"location":"documentation/transition-guide/#from-pnp-libraries","text":"import { Logger , LogLevel , FunctionListener } from \"@pnp/logging\" ; import * as Util from \"@pnp/core\" ; import { sp , Web } from \"@pnp/sp\" ; In the above example the \"sp\" import replaces \"pnp\" and is the root of your method chains. Once we have updated our imports we have a few small code changes to make, depending on how you have used the library in your applications. Watch this short video discussing the most common updates:","title":"From @pnp libraries"},{"location":"documentation/transition-guide/#updated-settings-file-format","text":"If you are doing local debugging or testing you have likely created a settings.js from the supplied settings.example.js. Please note the format of that file has changed, the new format is shown below. var settings = { spsave : { username : \"develina.devsson@mydevtenant.onmicrosoft.com\" , password : \"pass@word1\" , siteUrl : \"https://mydevtenant.sharepoint.com/\" }, testing : { enableWebTests : true , sp : { id : \"{ client id }\" , secret : \"{ client secret }\" , url : \"{ site collection url }\" , notificationUrl : \"{ notification url }\" , }, graph : { tenant : \"{tenant.onmicrosoft.com}\" , id : \"{your app id}\" , secret : \"{your secret}\" }, } }","title":"Updated settings file format"},{"location":"documentation/transition-guide/#httpclient-renamed","text":"If you used HttpClient from sp-pnp-js it was renamed to SPHttpClient. A transition to @pnp/sp assumes replacement of: import { HttpClient } from 'sp-pnp-js' ; to the following import statement: import { SPHttpClient } from '@pnp/sp' ;","title":"HttpClient Renamed"},{"location":"graph/docs/","text":"@pnp/graph \u00b6 This package contains the fluent api used to call the graph rest services. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application and access the root sp object import { graph } from \"@pnp/graph\" ; ( function main() { // here we will load the current web's properties graph . groups . get (). then ( g => { console . log ( `Groups: ${ JSON . stringify ( g , null , 4 ) } ` ); }); })() Getting Started with SharePoint Framework \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application, update OnInit, and access the root sp object in render import { graph } from \"@pnp/graph\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present graph . setup ({ spfxContext : this.context }); }); } // ... public render () : void { // A simple loading message this . domElement . innerHTML = `Loading...` ; // here we will load the current web's properties graph . groups . get (). then ( groups => { this . domElement . innerHTML = `Groups:
    ${ groups . map ( g => `
  • ${ g . displayName }
  • ` ). join ( \"\" ) }
` ; }); } Getting Started on Nodejs \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { graph } from \"@pnp/graph\" ; import { AdalFetchClient } from \"@pnp/nodejs\" ; // do this once per page load graph . setup ({ graph : { fetchClientFactory : () => { return new AdalFetchClient ( \"{tenant}.onmicrosoft.com\" , \"AAD Application Id\" , \"AAD Application Secret\" ); }, }, }); // here we will load the groups information graph . groups . get (). then ( g => { console . log ( `Groups: ${ JSON . stringify ( g , null , 4 ) } ` ); }); UML \u00b6 Graphical UML diagram of @pnp/graph. Right-click the diagram and open in new tab if it is too small.","title":"graph"},{"location":"graph/docs/#pnpgraph","text":"This package contains the fluent api used to call the graph rest services.","title":"@pnp/graph"},{"location":"graph/docs/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application and access the root sp object import { graph } from \"@pnp/graph\" ; ( function main() { // here we will load the current web's properties graph . groups . get (). then ( g => { console . log ( `Groups: ${ JSON . stringify ( g , null , 4 ) } ` ); }); })()","title":"Getting Started"},{"location":"graph/docs/#getting-started-with-sharepoint-framework","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application, update OnInit, and access the root sp object in render import { graph } from \"@pnp/graph\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present graph . setup ({ spfxContext : this.context }); }); } // ... public render () : void { // A simple loading message this . domElement . innerHTML = `Loading...` ; // here we will load the current web's properties graph . groups . get (). then ( groups => { this . domElement . innerHTML = `Groups:
    ${ groups . map ( g => `
  • ${ g . displayName }
  • ` ). join ( \"\" ) }
` ; }); }","title":"Getting Started with SharePoint Framework"},{"location":"graph/docs/#getting-started-on-nodejs","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { graph } from \"@pnp/graph\" ; import { AdalFetchClient } from \"@pnp/nodejs\" ; // do this once per page load graph . setup ({ graph : { fetchClientFactory : () => { return new AdalFetchClient ( \"{tenant}.onmicrosoft.com\" , \"AAD Application Id\" , \"AAD Application Secret\" ); }, }, }); // here we will load the groups information graph . groups . get (). then ( g => { console . log ( `Groups: ${ JSON . stringify ( g , null , 4 ) } ` ); });","title":"Getting Started on Nodejs"},{"location":"graph/docs/#uml","text":"Graphical UML diagram of @pnp/graph. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"graph/docs/contacts/","text":"@pnp/graph/contacts \u00b6 The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described you can add and edit both contacts and folders in a users Outlook. Get all of the Contacts \u00b6 Using the contacts() you can get the users contacts from Outlook import { graph } from \"@pnp/graph\" ; const contacts = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . get (); const contacts = await graph . me . contacts . get (); Add a new Contact \u00b6 Using the contacts.add() you can a add Contact to the users Outlook import { graph } from \"@pnp/graph\" ; const addedContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]); const addedContact = await graph . me . contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]); Get Contact by Id \u00b6 Using the contacts.getById() you can get one of the users Contacts in Outlook import { graph } from \"@pnp/graph\" ; const contact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . getById ( 'userId' ); const contact = await graph . me . contacts . getById ( 'userId' ); Delete a Contact \u00b6 Using the delete you can remove one of the users Contacts in Outlook import { graph } from \"@pnp/graph\" ; const delContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . getById ( 'userId' ). delete (); const delContact = await graph . me . contacts . getById ( 'userId' ). delete (); Update a Contact \u00b6 Using the update you can update one of the users Contacts in Outlook import { graph } from \"@pnp/graph\" ; const updContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . getById ( 'userId' ). update ({ birthday : \"1986-05-30\" }); const updContact = await graph . me . contacts . getById ( 'userId' ). update ({ birthday : \"1986-05-30\" }); Get all of the Contact Folders \u00b6 Using the contactFolders() you can get the users Contact Folders from Outlook import { graph } from \"@pnp/graph\" ; const contactFolders = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . get (); const contactFolders = await graph . me . contactFolders . get (); Add a new Contact Folder \u00b6 Using the contactFolders.add() you can a add Contact Folder to the users Outlook import { graph } from \"@pnp/graph\" ; const addedContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . add ( 'displayName' , '' ); const addedContactFolder = await graph . me . contactFolders . contactFolders . add ( 'displayName' , '' ); Get Contact Folder by Id \u00b6 Using the contactFolders.getById() you can get one of the users Contact Folders in Outlook import { graph } from \"@pnp/graph\" ; const contactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'folderId' ); const contactFolder = await graph . me . contactFolders . getById ( 'folderId' ); Delete a Contact Folder \u00b6 Using the delete you can remove one of the users Contact Folders in Outlook import { graph } from \"@pnp/graph\" ; const delContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'folderId' ). delete (); const delContactFolder = await graph . me . contactFolders . getById ( 'folderId' ). delete (); Update a Contact Folder \u00b6 Using the update you can update one of the users Contact Folders in Outlook import { graph } from \"@pnp/graph\" ; const updContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'userId' ). update ({ displayName : \"value\" }); const updContactFolder = await graph . me . contactFolders . getById ( 'userId' ). update ({ displayName : \"value\" }); Get all of the Contacts from the Contact Folder \u00b6 Using the contacts() in the Contact Folder gets the users Contact from the folder. import { graph } from \"@pnp/graph\" ; const contactsInContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'folderId' ). contacts . get (); const contactsInContactFolder = await graph . me . contactFolders . getById ( 'folderId' ). contacts . get (); Get Child Folders of the Contact Folder \u00b6 Using the childFolders() you can get the Child Folders of the current Contact Folder from Outlook import { graph } from \"@pnp/graph\" ; const childFolders = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . get (); const childFolders = await graph . me . contactFolders . getById ( '' ). childFolders . get (); Add a new Child Folder \u00b6 Using the childFolders.add() you can a add Child Folder in a Contact Folder import { graph } from \"@pnp/graph\" ; const addedChildFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . add ( 'displayName' , '' ); const addedChildFolder = await graph . me . contactFolders . getById ( '' ). childFolders . add ( 'displayName' , '' ); Get Child Folder by Id \u00b6 Using the childFolders.getById() you can get one of the users Child Folders in Outlook import { graph } from \"@pnp/graph\" ; const childFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ); const childFolder = await graph . me . contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ); Add Contact in Child Folder of Contact Folder \u00b6 Using contacts.add in the Child Folder of a Contact Folder, adds a new Contact to that folder import { graph } from \"@pnp/graph\" ; const addedContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ). contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]); const addedContact = await graph . me . contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ). contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]);","title":"contacts"},{"location":"graph/docs/contacts/#pnpgraphcontacts","text":"The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described you can add and edit both contacts and folders in a users Outlook.","title":"@pnp/graph/contacts"},{"location":"graph/docs/contacts/#get-all-of-the-contacts","text":"Using the contacts() you can get the users contacts from Outlook import { graph } from \"@pnp/graph\" ; const contacts = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . get (); const contacts = await graph . me . contacts . get ();","title":"Get all of the Contacts"},{"location":"graph/docs/contacts/#add-a-new-contact","text":"Using the contacts.add() you can a add Contact to the users Outlook import { graph } from \"@pnp/graph\" ; const addedContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]); const addedContact = await graph . me . contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]);","title":"Add a new Contact"},{"location":"graph/docs/contacts/#get-contact-by-id","text":"Using the contacts.getById() you can get one of the users Contacts in Outlook import { graph } from \"@pnp/graph\" ; const contact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . getById ( 'userId' ); const contact = await graph . me . contacts . getById ( 'userId' );","title":"Get Contact by Id"},{"location":"graph/docs/contacts/#delete-a-contact","text":"Using the delete you can remove one of the users Contacts in Outlook import { graph } from \"@pnp/graph\" ; const delContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . getById ( 'userId' ). delete (); const delContact = await graph . me . contacts . getById ( 'userId' ). delete ();","title":"Delete a Contact"},{"location":"graph/docs/contacts/#update-a-contact","text":"Using the update you can update one of the users Contacts in Outlook import { graph } from \"@pnp/graph\" ; const updContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contacts . getById ( 'userId' ). update ({ birthday : \"1986-05-30\" }); const updContact = await graph . me . contacts . getById ( 'userId' ). update ({ birthday : \"1986-05-30\" });","title":"Update a Contact"},{"location":"graph/docs/contacts/#get-all-of-the-contact-folders","text":"Using the contactFolders() you can get the users Contact Folders from Outlook import { graph } from \"@pnp/graph\" ; const contactFolders = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . get (); const contactFolders = await graph . me . contactFolders . get ();","title":"Get all of the Contact Folders"},{"location":"graph/docs/contacts/#add-a-new-contact-folder","text":"Using the contactFolders.add() you can a add Contact Folder to the users Outlook import { graph } from \"@pnp/graph\" ; const addedContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . add ( 'displayName' , '' ); const addedContactFolder = await graph . me . contactFolders . contactFolders . add ( 'displayName' , '' );","title":"Add a new Contact Folder"},{"location":"graph/docs/contacts/#get-contact-folder-by-id","text":"Using the contactFolders.getById() you can get one of the users Contact Folders in Outlook import { graph } from \"@pnp/graph\" ; const contactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'folderId' ); const contactFolder = await graph . me . contactFolders . getById ( 'folderId' );","title":"Get Contact Folder by Id"},{"location":"graph/docs/contacts/#delete-a-contact-folder","text":"Using the delete you can remove one of the users Contact Folders in Outlook import { graph } from \"@pnp/graph\" ; const delContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'folderId' ). delete (); const delContactFolder = await graph . me . contactFolders . getById ( 'folderId' ). delete ();","title":"Delete a Contact Folder"},{"location":"graph/docs/contacts/#update-a-contact-folder","text":"Using the update you can update one of the users Contact Folders in Outlook import { graph } from \"@pnp/graph\" ; const updContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'userId' ). update ({ displayName : \"value\" }); const updContactFolder = await graph . me . contactFolders . getById ( 'userId' ). update ({ displayName : \"value\" });","title":"Update a Contact Folder"},{"location":"graph/docs/contacts/#get-all-of-the-contacts-from-the-contact-folder","text":"Using the contacts() in the Contact Folder gets the users Contact from the folder. import { graph } from \"@pnp/graph\" ; const contactsInContactFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( 'folderId' ). contacts . get (); const contactsInContactFolder = await graph . me . contactFolders . getById ( 'folderId' ). contacts . get ();","title":"Get all of the Contacts from the Contact Folder"},{"location":"graph/docs/contacts/#get-child-folders-of-the-contact-folder","text":"Using the childFolders() you can get the Child Folders of the current Contact Folder from Outlook import { graph } from \"@pnp/graph\" ; const childFolders = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . get (); const childFolders = await graph . me . contactFolders . getById ( '' ). childFolders . get ();","title":"Get Child Folders of the Contact Folder"},{"location":"graph/docs/contacts/#add-a-new-child-folder","text":"Using the childFolders.add() you can a add Child Folder in a Contact Folder import { graph } from \"@pnp/graph\" ; const addedChildFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . add ( 'displayName' , '' ); const addedChildFolder = await graph . me . contactFolders . getById ( '' ). childFolders . add ( 'displayName' , '' );","title":"Add a new Child Folder"},{"location":"graph/docs/contacts/#get-child-folder-by-id","text":"Using the childFolders.getById() you can get one of the users Child Folders in Outlook import { graph } from \"@pnp/graph\" ; const childFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ); const childFolder = await graph . me . contactFolders . getById ( '' ). childFolders . getById ( 'folderId' );","title":"Get Child Folder by Id"},{"location":"graph/docs/contacts/#add-contact-in-child-folder-of-contact-folder","text":"Using contacts.add in the Child Folder of a Contact Folder, adds a new Contact to that folder import { graph } from \"@pnp/graph\" ; const addedContact = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ). contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]); const addedContact = await graph . me . contactFolders . getById ( '' ). childFolders . getById ( 'folderId' ). contacts . add ( 'Pavel' , 'Bansky' , [ < EmailAddress > { address : 'pavelb@fabrikam.onmicrosoft.com' , name : 'Pavel Bansky' }], [ '+1 732 555 0102' ]);","title":"Add Contact in Child Folder of Contact Folder"},{"location":"graph/docs/directoryobjects/","text":"@pnp/graph/directoryObjects \u00b6 The groups and directory roles for the user \u00b6 import { graph } from \"@pnp/graph\" ; const memberOf = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). memberOf . get (); const memberOf = await graph . me . memberOf . get (); Return all the groups the user, group or directoryObject is a member of \u00b6 import { graph } from \"@pnp/graph\" ; const memberGroups = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberGroups (); const memberGroups = await graph . me . getMemberGroups (); const memberGroups = await graph . groups . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberGroups (); const memberGroups = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberGroups (); Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. \u00b6 import { graph } from \"@pnp/graph\" ; const memberObjects = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberObjects (); const memberObjects = await graph . me . getMemberObjects (); const memberObjects = await graph . groups . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberObjects (); const memberObjects = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberObjects (); Check for membership in a specified list of groups \u00b6 And returns from that list those groups of which the specified user, group, or directory object is a member import { graph } from \"@pnp/graph\" ; const checkedMembers = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); const checkedMembers = await graph . me . checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); const checkedMembers = await graph . groups . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); const checkedMembers = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); Get directoryObject by Id \u00b6 import { graph } from \"@pnp/graph\" ; const dirObject = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). get (); Delete directoryObject \u00b6 import { graph } from \"@pnp/graph\" ; const deleted = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). delete ()","title":"directory objects"},{"location":"graph/docs/directoryobjects/#pnpgraphdirectoryobjects","text":"","title":"@pnp/graph/directoryObjects"},{"location":"graph/docs/directoryobjects/#the-groups-and-directory-roles-for-the-user","text":"import { graph } from \"@pnp/graph\" ; const memberOf = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). memberOf . get (); const memberOf = await graph . me . memberOf . get ();","title":"The groups and directory roles for the user"},{"location":"graph/docs/directoryobjects/#return-all-the-groups-the-user-group-or-directoryobject-is-a-member-of","text":"import { graph } from \"@pnp/graph\" ; const memberGroups = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberGroups (); const memberGroups = await graph . me . getMemberGroups (); const memberGroups = await graph . groups . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberGroups (); const memberGroups = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberGroups ();","title":"Return all the groups the user, group or directoryObject is a member of"},{"location":"graph/docs/directoryobjects/#returns-all-the-groups-administrative-units-and-directory-roles-that-a-user-group-or-directory-object-is-a-member-of","text":"import { graph } from \"@pnp/graph\" ; const memberObjects = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberObjects (); const memberObjects = await graph . me . getMemberObjects (); const memberObjects = await graph . groups . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberObjects (); const memberObjects = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). getMemberObjects ();","title":"Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of."},{"location":"graph/docs/directoryobjects/#check-for-membership-in-a-specified-list-of-groups","text":"And returns from that list those groups of which the specified user, group, or directory object is a member import { graph } from \"@pnp/graph\" ; const checkedMembers = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); const checkedMembers = await graph . me . checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); const checkedMembers = await graph . groups . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]); const checkedMembers = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). checkMemberGroups ([ \"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\" , \"2001bb09-1d46-40a6-8176-7bb867fb75aa\" ]);","title":"Check for membership in a specified list of groups"},{"location":"graph/docs/directoryobjects/#get-directoryobject-by-id","text":"import { graph } from \"@pnp/graph\" ; const dirObject = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). get ();","title":"Get directoryObject by Id"},{"location":"graph/docs/directoryobjects/#delete-directoryobject","text":"import { graph } from \"@pnp/graph\" ; const deleted = await graph . directoryObjects . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). delete ()","title":"Delete directoryObject"},{"location":"graph/docs/insights/","text":"@pnp/graph/insights \u00b6 Insights are relationships calculated using advanced analytics and machine learning techniques. You can, for example, identify OneDrive documents trending around users. Get the trending documents \u00b6 Using the trending() returns documents from OneDrive and from SharePoint sites trending around a user. import { graph } from \"@pnp/graph\" ; const trending = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). insights . trending . get (); const trending = await graph . me . insights . trending . get (); Get the used documents \u00b6 Using the used() returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive. import { graph } from \"@pnp/graph\" ; const used = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). insights . used . get (); const used = await graph . me . insights . used . get (); Get the shared documents \u00b6 Using the shared() returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails. import { graph } from \"@pnp/graph\" ; const shared = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). insights . shared . get (); const shared = await graph . me . insights . shared . get ();","title":"@pnp/graph/insights"},{"location":"graph/docs/insights/#pnpgraphinsights","text":"Insights are relationships calculated using advanced analytics and machine learning techniques. You can, for example, identify OneDrive documents trending around users.","title":"@pnp/graph/insights"},{"location":"graph/docs/insights/#get-the-trending-documents","text":"Using the trending() returns documents from OneDrive and from SharePoint sites trending around a user. import { graph } from \"@pnp/graph\" ; const trending = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). insights . trending . get (); const trending = await graph . me . insights . trending . get ();","title":"Get the trending documents"},{"location":"graph/docs/insights/#get-the-used-documents","text":"Using the used() returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive. import { graph } from \"@pnp/graph\" ; const used = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). insights . used . get (); const used = await graph . me . insights . used . get ();","title":"Get the used documents"},{"location":"graph/docs/insights/#get-the-shared-documents","text":"Using the shared() returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails. import { graph } from \"@pnp/graph\" ; const shared = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). insights . shared . get (); const shared = await graph . me . insights . shared . get ();","title":"Get the shared documents"},{"location":"graph/docs/invitations/","text":"@pnp/graph/invitations \u00b6 The ability invite an external user via the invitation manager Create Invitation \u00b6 Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). import { graph } from \"@pnp/graph\" ; const invitationResult = await graph . invitations . create ( 'external.user@emailadress.com' , 'https://tenant.sharepoint.com/sites/redirecturi' );","title":"invitations"},{"location":"graph/docs/invitations/#pnpgraphinvitations","text":"The ability invite an external user via the invitation manager","title":"@pnp/graph/invitations"},{"location":"graph/docs/invitations/#create-invitation","text":"Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). import { graph } from \"@pnp/graph\" ; const invitationResult = await graph . invitations . create ( 'external.user@emailadress.com' , 'https://tenant.sharepoint.com/sites/redirecturi' );","title":"Create Invitation"},{"location":"graph/docs/onedrive/","text":"@pnp/graph/onedrive \u00b6 The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive. Get the default drive \u00b6 Using the drive() you can get the default drive from Onedrive import { graph } from \"@pnp/graph\" ; const drives = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . get (); const drives = await graph . me . drives . get (); Get all of the drives \u00b6 Using the drives() you can get the users available drives from Onedrive import { graph } from \"@pnp/graph\" ; const drives = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . get (); const drives = await graph . me . drives . get (); Get drive by Id \u00b6 Using the drives.getById() you can get one of the available drives in Outlook import { graph } from \"@pnp/graph\" ; const drive = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ); const drive = await graph . me . drives . getById ( 'driveId' ); Get the associated list of a drive \u00b6 Using the list() you get the associated list import { graph } from \"@pnp/graph\" ; const list = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). list . get (); const list = await graph . me . drives . getById ( 'driveId' ). list . get (); Get the recent files \u00b6 Using the recent() you get the recent files import { graph } from \"@pnp/graph\" ; const files = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). recent . get (); const files = await graph . me . drives . getById ( 'driveId' ). recent . get (); Get the files shared with me \u00b6 Using the sharedWithMe() you get the files shared with the user import { graph } from \"@pnp/graph\" ; const shared = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). sharedWithMe . get (); const shared = await graph . me . drives . getById ( 'driveId' ). sharedWithMe . get (); Get the Root folder \u00b6 Using the root() you get the root folder import { graph } from \"@pnp/graph\" ; const root = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). root . get (); const root = await graph . me . drives . getById ( 'driveId' ). root . get (); Get the Children \u00b6 Using the children() you get the children import { graph } from \"@pnp/graph\" ; const rootChildren = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). root . children . get (); const rootChildren = await graph . me . drives . getById ( 'driveId' ). root . children . get (); const itemChildren = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). children . get (); const itemChildren = await graph . me . drives . getById ( 'driveId' ). root . items . getById ( 'itemId' ). children . get (); Add folder or item \u00b6 Using the add you can add a folder or an item import { graph } from \"@pnp/graph\" ; import { DriveItem as IDriveItem } from \"@microsoft/microsoft-graph-types\" ; const addFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). root . children . add ( 'New Folder' , < IDriveItem > { folder : {}}); const addFolder = await graph . me . drives . getById ( 'driveId' ). root . children . add ( 'New Folder' , < IDriveItem > { folder : {}}); Search items \u00b6 Using the search() you can search for items, and optionally select properties import { graph } from \"@pnp/graph\" ; const search = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ) root . search ( 'queryText' ). get (); const search = await graph . me . drives . getById ( 'driveId' ) root . search ( 'queryText' ). get (); Get specific item in drive \u00b6 Using the items.getById() you can get a specific item from the current drive import { graph } from \"@pnp/graph\" ; const item = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ); const item = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ); Get thumbnails \u00b6 Using the thumbnails() you get the thumbnails import { graph } from \"@pnp/graph\" ; const thumbs = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). thumbnails . get (); const thumbs = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). thumbnails . get (); Delete drive item \u00b6 Using the delete() you delete the current item import { graph } from \"@pnp/graph\" ; const thumbs = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). delete (); const thumbs = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). delete (); Update drive item \u00b6 Using the update() you update the current item import { graph } from \"@pnp/graph\" ; const update = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). update ({ name : \"New Name\" }); const update = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). update ({ name : \"New Name\" }); Move drive item \u00b6 Using the move() you move the current item, and optionally update it import { graph } from \"@pnp/graph\" ; // Requires a parentReference to the new folder location const move = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). move ({ parentReference : { id : 'itemId' }}, { name : \"New Name\" }); const move = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). move ({ parentReference : { id : 'itemId' }}, { name : \"New Name\" });","title":"onedrive"},{"location":"graph/docs/onedrive/#pnpgraphonedrive","text":"The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive.","title":"@pnp/graph/onedrive"},{"location":"graph/docs/onedrive/#get-the-default-drive","text":"Using the drive() you can get the default drive from Onedrive import { graph } from \"@pnp/graph\" ; const drives = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . get (); const drives = await graph . me . drives . get ();","title":"Get the default drive"},{"location":"graph/docs/onedrive/#get-all-of-the-drives","text":"Using the drives() you can get the users available drives from Onedrive import { graph } from \"@pnp/graph\" ; const drives = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . get (); const drives = await graph . me . drives . get ();","title":"Get all of the drives"},{"location":"graph/docs/onedrive/#get-drive-by-id","text":"Using the drives.getById() you can get one of the available drives in Outlook import { graph } from \"@pnp/graph\" ; const drive = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ); const drive = await graph . me . drives . getById ( 'driveId' );","title":"Get drive by Id"},{"location":"graph/docs/onedrive/#get-the-associated-list-of-a-drive","text":"Using the list() you get the associated list import { graph } from \"@pnp/graph\" ; const list = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). list . get (); const list = await graph . me . drives . getById ( 'driveId' ). list . get ();","title":"Get the associated list of a drive"},{"location":"graph/docs/onedrive/#get-the-recent-files","text":"Using the recent() you get the recent files import { graph } from \"@pnp/graph\" ; const files = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). recent . get (); const files = await graph . me . drives . getById ( 'driveId' ). recent . get ();","title":"Get the recent files"},{"location":"graph/docs/onedrive/#get-the-files-shared-with-me","text":"Using the sharedWithMe() you get the files shared with the user import { graph } from \"@pnp/graph\" ; const shared = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). sharedWithMe . get (); const shared = await graph . me . drives . getById ( 'driveId' ). sharedWithMe . get ();","title":"Get the files shared with me"},{"location":"graph/docs/onedrive/#get-the-root-folder","text":"Using the root() you get the root folder import { graph } from \"@pnp/graph\" ; const root = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). root . get (); const root = await graph . me . drives . getById ( 'driveId' ). root . get ();","title":"Get the Root folder"},{"location":"graph/docs/onedrive/#get-the-children","text":"Using the children() you get the children import { graph } from \"@pnp/graph\" ; const rootChildren = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). root . children . get (); const rootChildren = await graph . me . drives . getById ( 'driveId' ). root . children . get (); const itemChildren = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). children . get (); const itemChildren = await graph . me . drives . getById ( 'driveId' ). root . items . getById ( 'itemId' ). children . get ();","title":"Get the Children"},{"location":"graph/docs/onedrive/#add-folder-or-item","text":"Using the add you can add a folder or an item import { graph } from \"@pnp/graph\" ; import { DriveItem as IDriveItem } from \"@microsoft/microsoft-graph-types\" ; const addFolder = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). root . children . add ( 'New Folder' , < IDriveItem > { folder : {}}); const addFolder = await graph . me . drives . getById ( 'driveId' ). root . children . add ( 'New Folder' , < IDriveItem > { folder : {}});","title":"Add folder or item"},{"location":"graph/docs/onedrive/#search-items","text":"Using the search() you can search for items, and optionally select properties import { graph } from \"@pnp/graph\" ; const search = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ) root . search ( 'queryText' ). get (); const search = await graph . me . drives . getById ( 'driveId' ) root . search ( 'queryText' ). get ();","title":"Search items"},{"location":"graph/docs/onedrive/#get-specific-item-in-drive","text":"Using the items.getById() you can get a specific item from the current drive import { graph } from \"@pnp/graph\" ; const item = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ); const item = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' );","title":"Get specific item in drive"},{"location":"graph/docs/onedrive/#get-thumbnails","text":"Using the thumbnails() you get the thumbnails import { graph } from \"@pnp/graph\" ; const thumbs = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). thumbnails . get (); const thumbs = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). thumbnails . get ();","title":"Get thumbnails"},{"location":"graph/docs/onedrive/#delete-drive-item","text":"Using the delete() you delete the current item import { graph } from \"@pnp/graph\" ; const thumbs = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). delete (); const thumbs = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). delete ();","title":"Delete drive item"},{"location":"graph/docs/onedrive/#update-drive-item","text":"Using the update() you update the current item import { graph } from \"@pnp/graph\" ; const update = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). update ({ name : \"New Name\" }); const update = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). update ({ name : \"New Name\" });","title":"Update drive item"},{"location":"graph/docs/onedrive/#move-drive-item","text":"Using the move() you move the current item, and optionally update it import { graph } from \"@pnp/graph\" ; // Requires a parentReference to the new folder location const move = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). drives . getById ( 'driveId' ). items . getById ( 'itemId' ). move ({ parentReference : { id : 'itemId' }}, { name : \"New Name\" }); const move = await graph . me . drives . getById ( 'driveId' ). items . getById ( 'itemId' ). move ({ parentReference : { id : 'itemId' }}, { name : \"New Name\" });","title":"Move drive item"},{"location":"graph/docs/people/","text":"@pnp/graph/people \u00b6 The ability to retrieve a list of person objects ordered by their relevance to the user, which is determined by the user's communication and collaboration patterns, and business relationships. Get all of the people \u00b6 Using the people() you can retrieve a list of person objects ordered by their relevance to the user. import { graph } from \"@pnp/graph\" ; const people = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). people . get (); const people = await graph . me . people . get ();","title":"@pnp/graph/people"},{"location":"graph/docs/people/#pnpgraphpeople","text":"The ability to retrieve a list of person objects ordered by their relevance to the user, which is determined by the user's communication and collaboration patterns, and business relationships.","title":"@pnp/graph/people"},{"location":"graph/docs/people/#get-all-of-the-people","text":"Using the people() you can retrieve a list of person objects ordered by their relevance to the user. import { graph } from \"@pnp/graph\" ; const people = await graph . users . getById ( 'user@tenant.onmicrosoft.com' ). people . get (); const people = await graph . me . people . get ();","title":"Get all of the people"},{"location":"graph/docs/planner/","text":"@pnp/graph/planner \u00b6 The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner. Get Plans by Id \u00b6 Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan. import { graph } from \"@pnp/graph\" ; const plan = await graph . planner . plans . getById ( 'planId' ); Add new Plan \u00b6 Using the planner.plans.add() you can create a new Plan. import { graph } from \"@pnp/graph\" ; const newPlan = await graph . planner . plans . add ( 'groupObjectId' , 'title' ); Get Tasks in Plan \u00b6 Using the tasks() you can get the Tasks in a Plan. import { graph } from \"@pnp/graph\" ; const planTasks = await graph . planner . plans . getById ( 'planId' ). tasks . get (); Get Buckets in Plan \u00b6 Using the buckets() you can get the Buckets in a Plan. import { graph } from \"@pnp/graph\" ; const planBuckets = await graph . planner . plans . getById ( 'planId' ). buckets . get (); Get Details in Plan \u00b6 Using the details() you can get the details in a Plan. import { graph } from \"@pnp/graph\" ; const planDetails = await graph . planner . plans . getById ( 'planId' ). details . get (); Delete Plan \u00b6 Using the delete() you can get delete a Plan. import { graph } from \"@pnp/graph\" ; const delPlan = await graph . planner . plans . getById ( 'planId' ). delete (); Update Plan \u00b6 Using the update() you can get update a Plan. import { graph } from \"@pnp/graph\" ; const updPlan = await graph . planner . plans . getById ( 'planId' ). update ({ title : 'New Title' }); Get Task by Id \u00b6 Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task. import { graph } from \"@pnp/graph\" ; const task = await graph . planner . tasks . getById ( 'taskId' ); Add new Task \u00b6 Using the planner.tasks.add() you can create a new Task. import { graph } from \"@pnp/graph\" ; const newTask = await graph . planner . tasks . add ( 'planId' , 'title' ); Get Details in Task \u00b6 Using the details() you can get the details in a Task. import { graph } from \"@pnp/graph\" ; const taskDetails = await graph . planner . tasks . getById ( 'taskId' ). details . get (); Delete Task \u00b6 Using the delete() you can get delete a Task. import { graph } from \"@pnp/graph\" ; const delTask = await graph . planner . tasks . getById ( 'taskId' ). delete (); Update Task \u00b6 Using the update() you can get update a Task. import { graph } from \"@pnp/graph\" ; const updTask = await graph . planner . tasks . getById ( 'taskId' ). update ({ properties }); Get Buckets by Id \u00b6 Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket. import { graph } from \"@pnp/graph\" ; const bucket = await graph . planner . buckets . getById ( 'bucketId' ); Add new Bucket \u00b6 Using the planner.buckets.add() you can create a new Bucket. import { graph } from \"@pnp/graph\" ; const newBucket = await graph . planner . buckets . add ( 'name' , 'planId' ); Update Bucket \u00b6 Using the update() you can get update a Bucket. import { graph } from \"@pnp/graph\" ; const updBucket = await graph . planner . buckets . getById ( 'bucketId' ). update ({ name : \"Name\" }); Delete Bucket \u00b6 Using the delete() you can get delete a Bucket. import { graph } from \"@pnp/graph\" ; const delBucket = await graph . planner . buckets . getById ( 'bucketId' ). delete (); Get Bucket Tasks \u00b6 Using the tasks() you can get Tasks in a Bucket. import { graph } from \"@pnp/graph\" ; const bucketTasks = await graph . planner . buckets . getById ( 'bucketId' ). tasks . get ();","title":"planner"},{"location":"graph/docs/planner/#pnpgraphplanner","text":"The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner.","title":"@pnp/graph/planner"},{"location":"graph/docs/planner/#get-plans-by-id","text":"Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan. import { graph } from \"@pnp/graph\" ; const plan = await graph . planner . plans . getById ( 'planId' );","title":"Get Plans by Id"},{"location":"graph/docs/planner/#add-new-plan","text":"Using the planner.plans.add() you can create a new Plan. import { graph } from \"@pnp/graph\" ; const newPlan = await graph . planner . plans . add ( 'groupObjectId' , 'title' );","title":"Add new Plan"},{"location":"graph/docs/planner/#get-tasks-in-plan","text":"Using the tasks() you can get the Tasks in a Plan. import { graph } from \"@pnp/graph\" ; const planTasks = await graph . planner . plans . getById ( 'planId' ). tasks . get ();","title":"Get Tasks in Plan"},{"location":"graph/docs/planner/#get-buckets-in-plan","text":"Using the buckets() you can get the Buckets in a Plan. import { graph } from \"@pnp/graph\" ; const planBuckets = await graph . planner . plans . getById ( 'planId' ). buckets . get ();","title":"Get Buckets in Plan"},{"location":"graph/docs/planner/#get-details-in-plan","text":"Using the details() you can get the details in a Plan. import { graph } from \"@pnp/graph\" ; const planDetails = await graph . planner . plans . getById ( 'planId' ). details . get ();","title":"Get Details in Plan"},{"location":"graph/docs/planner/#delete-plan","text":"Using the delete() you can get delete a Plan. import { graph } from \"@pnp/graph\" ; const delPlan = await graph . planner . plans . getById ( 'planId' ). delete ();","title":"Delete Plan"},{"location":"graph/docs/planner/#update-plan","text":"Using the update() you can get update a Plan. import { graph } from \"@pnp/graph\" ; const updPlan = await graph . planner . plans . getById ( 'planId' ). update ({ title : 'New Title' });","title":"Update Plan"},{"location":"graph/docs/planner/#get-task-by-id","text":"Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task. import { graph } from \"@pnp/graph\" ; const task = await graph . planner . tasks . getById ( 'taskId' );","title":"Get Task by Id"},{"location":"graph/docs/planner/#add-new-task","text":"Using the planner.tasks.add() you can create a new Task. import { graph } from \"@pnp/graph\" ; const newTask = await graph . planner . tasks . add ( 'planId' , 'title' );","title":"Add new Task"},{"location":"graph/docs/planner/#get-details-in-task","text":"Using the details() you can get the details in a Task. import { graph } from \"@pnp/graph\" ; const taskDetails = await graph . planner . tasks . getById ( 'taskId' ). details . get ();","title":"Get Details in Task"},{"location":"graph/docs/planner/#delete-task","text":"Using the delete() you can get delete a Task. import { graph } from \"@pnp/graph\" ; const delTask = await graph . planner . tasks . getById ( 'taskId' ). delete ();","title":"Delete Task"},{"location":"graph/docs/planner/#update-task","text":"Using the update() you can get update a Task. import { graph } from \"@pnp/graph\" ; const updTask = await graph . planner . tasks . getById ( 'taskId' ). update ({ properties });","title":"Update Task"},{"location":"graph/docs/planner/#get-buckets-by-id","text":"Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket. import { graph } from \"@pnp/graph\" ; const bucket = await graph . planner . buckets . getById ( 'bucketId' );","title":"Get Buckets by Id"},{"location":"graph/docs/planner/#add-new-bucket","text":"Using the planner.buckets.add() you can create a new Bucket. import { graph } from \"@pnp/graph\" ; const newBucket = await graph . planner . buckets . add ( 'name' , 'planId' );","title":"Add new Bucket"},{"location":"graph/docs/planner/#update-bucket","text":"Using the update() you can get update a Bucket. import { graph } from \"@pnp/graph\" ; const updBucket = await graph . planner . buckets . getById ( 'bucketId' ). update ({ name : \"Name\" });","title":"Update Bucket"},{"location":"graph/docs/planner/#delete-bucket","text":"Using the delete() you can get delete a Bucket. import { graph } from \"@pnp/graph\" ; const delBucket = await graph . planner . buckets . getById ( 'bucketId' ). delete ();","title":"Delete Bucket"},{"location":"graph/docs/planner/#get-bucket-tasks","text":"Using the tasks() you can get Tasks in a Bucket. import { graph } from \"@pnp/graph\" ; const bucketTasks = await graph . planner . buckets . getById ( 'bucketId' ). tasks . get ();","title":"Get Bucket Tasks"},{"location":"graph/docs/security/","text":"@pnp/graph/security \u00b6 The Microsoft Graph Security API can be used as a federated security aggregation service to submit queries to all onboarded security providers to get aggregated responses. Get all Alerts \u00b6 Using the alerts() to retrieve a list of Alert objects import { graph } from \"@pnp/graph\" ; const alerts = await graph . security . alerts . get (); Get an Alert by Id \u00b6 Using the alerts.getById() to retrieve a specific Alert object import { graph } from \"@pnp/graph\" ; const alert = await graph . security . alerts . getById ( 'alertId' ). get (); Update an Alert \u00b6 Using the alerts.getById().update() to retrieve a specific Alert object import { graph } from \"@pnp/graph\" ; const updAlert = await graph . security . alerts . getById ( 'alertId' ). update ({ status : 'Status' });","title":"@pnp/graph/security"},{"location":"graph/docs/security/#pnpgraphsecurity","text":"The Microsoft Graph Security API can be used as a federated security aggregation service to submit queries to all onboarded security providers to get aggregated responses.","title":"@pnp/graph/security"},{"location":"graph/docs/security/#get-all-alerts","text":"Using the alerts() to retrieve a list of Alert objects import { graph } from \"@pnp/graph\" ; const alerts = await graph . security . alerts . get ();","title":"Get all Alerts"},{"location":"graph/docs/security/#get-an-alert-by-id","text":"Using the alerts.getById() to retrieve a specific Alert object import { graph } from \"@pnp/graph\" ; const alert = await graph . security . alerts . getById ( 'alertId' ). get ();","title":"Get an Alert by Id"},{"location":"graph/docs/security/#update-an-alert","text":"Using the alerts.getById().update() to retrieve a specific Alert object import { graph } from \"@pnp/graph\" ; const updAlert = await graph . security . alerts . getById ( 'alertId' ). update ({ status : 'Status' });","title":"Update an Alert"},{"location":"graph/docs/sites/","text":"@pnp/graph/sites \u00b6 The ability to manage sites, lists and listitems in SharePoint is a capability introduced in version 1.3.0 of @pnp/graph. Get the Root Site \u00b6 Using the sites.root()() you can get the tenant root site import { graph } from \"@pnp/graph\" ; const tenantRootSite = await graph . sites . root . get () Get the Root Site by Id \u00b6 Using the sites.getById()() you can get the root site as well import { graph } from \"@pnp/graph\" ; const tenantRootSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). get () Access a Site by server-relative URL \u00b6 Using the sites.getById()() you can get a specific site. With the combination of the base URL and a relative URL. We are using an internal method for combining the URL in the right combination, with : ex: contoso.sharepoint.com:/sites/site1: Here are a few url combinations that works: import { graph } from \"@pnp/graph\" ; // No / in the URLs const siteByRelativeUrl = await graph . sites . getById ( 'contoso.sharepoint.com' , 'sites/site1' ). get () // Both trailing / in the base URL and starting / in the relative URL const siteByRelativeUrl = await graph . sites . getById ( 'contoso.sharepoint.com/' , '/sites/site1' ). get () // Both trailing / in the base URL and starting and trailing / in the relative URL const siteByRelativeUrl = await graph . sites . getById ( 'contoso.sharepoint.com/' , '/sites/site1/' ). get () Get the Sub Sites in a Site \u00b6 Using the sites()() you can get the sub sites of a site. As this is returned as Sites, you could use getById() for a specific site and use the operations. import { graph } from \"@pnp/graph\" ; const subsites = await graph . sites . getById ( 'contoso.sharepoint.com' ). sites . get (); Get Content Types \u00b6 Using the contentTypes()() you can get the Content Types from a Site or from a List import { graph } from \"@pnp/graph\" ; const contentTypesFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). contentTypes . get (); const contentTypesFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). contentTypes . get (); Get Specific Content Type \u00b6 Using the getById() you can get a specific Content Type from a Site or from a List import { graph } from \"@pnp/graph\" ; const contentTypeFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). contentTypes . getById ( 'contentTypeId' ). get (); const contentTypeFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). contentTypes . getById ( 'contentTypeId' ). get (); Get the Lists in a Site \u00b6 Using the lists() you can get the lists of a site. import { graph } from \"@pnp/graph\" ; const lists = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . get (); Get a specific List in a Site \u00b6 Using the lists.getById() you can get the lists of a site. import { graph } from \"@pnp/graph\" ; const list = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). get (); Create a Lists in a Site \u00b6 Using the lists.create() you can create a list in a site. import { graph } from \"@pnp/graph\" ; const newLists = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . create ( 'DisplayName' , { contentTypesEnabled : true , hidden : false , template : \"genericList\" }) Get the default drive \u00b6 Using the drive() you can get the default drive from a Site or a List import { graph } from \"@pnp/graph\" ; const drive = await graph . sites . getById ( 'contoso.sharepoint.com' ). drive . get (); const drive = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). drive . get (); Get all of the drives \u00b6 Using the drives() you can get the drives from the Site import { graph } from \"@pnp/graph\" ; const drives = await graph . sites . getById ( 'contoso.sharepoint.com' ). drives . get (); Get drive by Id \u00b6 Using the drives.getById() you can get one specific Drive. For more operations make sure to have a look in the onedrive documentation. import { graph } from \"@pnp/graph\" ; const drive = await raph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). drives . getById ( 'driveId' ). get (); Get Columns \u00b6 Using the columns() you can get the columns from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnsFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . get (); const columnsFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . get (); Get Specific Column \u00b6 Using the columns.getById() you can get a specific column from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . getById ( 'columnId' ). get (); const columnsFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . getById ( 'columnId' ). get (); Get Column Links \u00b6 Using the column.columnLinks() you can get the column links for a specific column, from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnLinksFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . getById ( 'columnId' ). columnLinks . get (); const columnLinksFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . getById ( 'columnId' ). columnLinks . get (); Get Column Link \u00b6 Using the column.columnLinks().getById() you can get a specific column link for a specific column, from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnLinkFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . getById ( 'columnId' ). columnLinks . getById ( 'columnLinkId' ). get (); const columnLinkFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . getById ( 'columnId' ). columnLinks . getById ( 'columnLinkId' ). get (); Get Items \u00b6 Using the items() you can get the Items from a List import { graph } from \"@pnp/graph\" ; const itemsFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . get (); Get Specific Item \u00b6 Using the getById()() you can get a specific Item from a List import { graph } from \"@pnp/graph\" ; const itemFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). get (); Create Item \u00b6 Using the items.create() you can create an Item in a List. import { graph } from \"@pnp/graph\" ; const newItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . create ({ \"Title\" : \"Widget\" , \"Color\" : \"Purple\" , \"Weight\" : 32 }); Update Item \u00b6 Using the update() you can update an Item in a List. import { graph } from \"@pnp/graph\" ; const Item = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). update ({ { \"Color\" : \"Fuchsia\" } }) Delete Item \u00b6 Using the delete() you can delete an Item in a List. import { graph } from \"@pnp/graph\" ; const Item = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). delete () Get Fields from Item \u00b6 Using the fields() you can the Fields in an Item import { graph } from \"@pnp/graph\" ; const fieldsFromItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). fields . get (); Get Versions from Item \u00b6 Using the versions() you can the Versions of an Item import { graph } from \"@pnp/graph\" ; const versionsFromItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). versions . get (); Get Version from Item \u00b6 Using the versions.getById()() you can the Versions of an Item import { graph } from \"@pnp/graph\" ; const versionFromItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). versions . getById ( 'versionId' ). get ();","title":"@pnp/graph/sites"},{"location":"graph/docs/sites/#pnpgraphsites","text":"The ability to manage sites, lists and listitems in SharePoint is a capability introduced in version 1.3.0 of @pnp/graph.","title":"@pnp/graph/sites"},{"location":"graph/docs/sites/#get-the-root-site","text":"Using the sites.root()() you can get the tenant root site import { graph } from \"@pnp/graph\" ; const tenantRootSite = await graph . sites . root . get ()","title":"Get the Root Site"},{"location":"graph/docs/sites/#get-the-root-site-by-id","text":"Using the sites.getById()() you can get the root site as well import { graph } from \"@pnp/graph\" ; const tenantRootSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). get ()","title":"Get the Root Site by Id"},{"location":"graph/docs/sites/#access-a-site-by-server-relative-url","text":"Using the sites.getById()() you can get a specific site. With the combination of the base URL and a relative URL. We are using an internal method for combining the URL in the right combination, with : ex: contoso.sharepoint.com:/sites/site1: Here are a few url combinations that works: import { graph } from \"@pnp/graph\" ; // No / in the URLs const siteByRelativeUrl = await graph . sites . getById ( 'contoso.sharepoint.com' , 'sites/site1' ). get () // Both trailing / in the base URL and starting / in the relative URL const siteByRelativeUrl = await graph . sites . getById ( 'contoso.sharepoint.com/' , '/sites/site1' ). get () // Both trailing / in the base URL and starting and trailing / in the relative URL const siteByRelativeUrl = await graph . sites . getById ( 'contoso.sharepoint.com/' , '/sites/site1/' ). get ()","title":"Access a Site by server-relative URL"},{"location":"graph/docs/sites/#get-the-sub-sites-in-a-site","text":"Using the sites()() you can get the sub sites of a site. As this is returned as Sites, you could use getById() for a specific site and use the operations. import { graph } from \"@pnp/graph\" ; const subsites = await graph . sites . getById ( 'contoso.sharepoint.com' ). sites . get ();","title":"Get the Sub Sites in a Site"},{"location":"graph/docs/sites/#get-content-types","text":"Using the contentTypes()() you can get the Content Types from a Site or from a List import { graph } from \"@pnp/graph\" ; const contentTypesFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). contentTypes . get (); const contentTypesFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). contentTypes . get ();","title":"Get Content Types"},{"location":"graph/docs/sites/#get-specific-content-type","text":"Using the getById() you can get a specific Content Type from a Site or from a List import { graph } from \"@pnp/graph\" ; const contentTypeFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). contentTypes . getById ( 'contentTypeId' ). get (); const contentTypeFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). contentTypes . getById ( 'contentTypeId' ). get ();","title":"Get Specific Content Type"},{"location":"graph/docs/sites/#get-the-lists-in-a-site","text":"Using the lists() you can get the lists of a site. import { graph } from \"@pnp/graph\" ; const lists = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . get ();","title":"Get the Lists in a Site"},{"location":"graph/docs/sites/#get-a-specific-list-in-a-site","text":"Using the lists.getById() you can get the lists of a site. import { graph } from \"@pnp/graph\" ; const list = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). get ();","title":"Get a specific List in a Site"},{"location":"graph/docs/sites/#create-a-lists-in-a-site","text":"Using the lists.create() you can create a list in a site. import { graph } from \"@pnp/graph\" ; const newLists = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . create ( 'DisplayName' , { contentTypesEnabled : true , hidden : false , template : \"genericList\" })","title":"Create a Lists in a Site"},{"location":"graph/docs/sites/#get-the-default-drive","text":"Using the drive() you can get the default drive from a Site or a List import { graph } from \"@pnp/graph\" ; const drive = await graph . sites . getById ( 'contoso.sharepoint.com' ). drive . get (); const drive = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). drive . get ();","title":"Get the default drive"},{"location":"graph/docs/sites/#get-all-of-the-drives","text":"Using the drives() you can get the drives from the Site import { graph } from \"@pnp/graph\" ; const drives = await graph . sites . getById ( 'contoso.sharepoint.com' ). drives . get ();","title":"Get all of the drives"},{"location":"graph/docs/sites/#get-drive-by-id","text":"Using the drives.getById() you can get one specific Drive. For more operations make sure to have a look in the onedrive documentation. import { graph } from \"@pnp/graph\" ; const drive = await raph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). drives . getById ( 'driveId' ). get ();","title":"Get drive by Id"},{"location":"graph/docs/sites/#get-columns","text":"Using the columns() you can get the columns from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnsFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . get (); const columnsFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . get ();","title":"Get Columns"},{"location":"graph/docs/sites/#get-specific-column","text":"Using the columns.getById() you can get a specific column from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . getById ( 'columnId' ). get (); const columnsFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . getById ( 'columnId' ). get ();","title":"Get Specific Column"},{"location":"graph/docs/sites/#get-column-links","text":"Using the column.columnLinks() you can get the column links for a specific column, from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnLinksFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . getById ( 'columnId' ). columnLinks . get (); const columnLinksFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . getById ( 'columnId' ). columnLinks . get ();","title":"Get Column Links"},{"location":"graph/docs/sites/#get-column-link","text":"Using the column.columnLinks().getById() you can get a specific column link for a specific column, from a Site or from a List import { graph } from \"@pnp/graph\" ; const columnLinkFromSite = await graph . sites . getById ( 'contoso.sharepoint.com' ). columns . getById ( 'columnId' ). columnLinks . getById ( 'columnLinkId' ). get (); const columnLinkFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). columns . getById ( 'columnId' ). columnLinks . getById ( 'columnLinkId' ). get ();","title":"Get Column Link"},{"location":"graph/docs/sites/#get-items","text":"Using the items() you can get the Items from a List import { graph } from \"@pnp/graph\" ; const itemsFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . get ();","title":"Get Items"},{"location":"graph/docs/sites/#get-specific-item","text":"Using the getById()() you can get a specific Item from a List import { graph } from \"@pnp/graph\" ; const itemFromList = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). get ();","title":"Get Specific Item"},{"location":"graph/docs/sites/#create-item","text":"Using the items.create() you can create an Item in a List. import { graph } from \"@pnp/graph\" ; const newItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . create ({ \"Title\" : \"Widget\" , \"Color\" : \"Purple\" , \"Weight\" : 32 });","title":"Create Item"},{"location":"graph/docs/sites/#update-item","text":"Using the update() you can update an Item in a List. import { graph } from \"@pnp/graph\" ; const Item = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). update ({ { \"Color\" : \"Fuchsia\" } })","title":"Update Item"},{"location":"graph/docs/sites/#delete-item","text":"Using the delete() you can delete an Item in a List. import { graph } from \"@pnp/graph\" ; const Item = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). delete ()","title":"Delete Item"},{"location":"graph/docs/sites/#get-fields-from-item","text":"Using the fields() you can the Fields in an Item import { graph } from \"@pnp/graph\" ; const fieldsFromItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). fields . get ();","title":"Get Fields from Item"},{"location":"graph/docs/sites/#get-versions-from-item","text":"Using the versions() you can the Versions of an Item import { graph } from \"@pnp/graph\" ; const versionsFromItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). versions . get ();","title":"Get Versions from Item"},{"location":"graph/docs/sites/#get-version-from-item","text":"Using the versions.getById()() you can the Versions of an Item import { graph } from \"@pnp/graph\" ; const versionFromItem = await graph . sites . getById ( 'contoso.sharepoint.com' ). lists . getById ( 'listId' ). items . getById ( 'itemId' ). versions . getById ( 'versionId' ). get ();","title":"Get Version from Item"},{"location":"graph/docs/subscriptions/","text":"@pnp/graph/subscriptions \u00b6 The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: Mail, events, and contacts from Outlook. Conversations from Office Groups. Drive root items from OneDrive. Users and Groups from Azure Active Directory. * Alerts from the Microsoft Graph Security API. Get all of the Subscriptions \u00b6 Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. import { graph } from \"@pnp/graph\" ; const subscriptions = await graph . subscriptions . get (); Create a new Subscription \u00b6 Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url. import { graph } from \"@pnp/graph\" ; const addedSubscription = await graph . subscriptions . add ( \"created,updated\" , \"https://webhook.azurewebsites.net/api/send/myNotifyClient\" , \"me/mailFolders('Inbox')/messages\" , \"2019-11-20T18:23:45.9356913Z\" ); Get Subscription by Id \u00b6 Using the subscriptions.getById() you can get one of the subscriptions import { graph } from \"@pnp/graph\" ; const subscription = await graph . subscriptions . getById ( 'subscriptionId' ); Delete a Subscription \u00b6 Using the subscriptions.getById().delete() you can remove one of the Subscriptions import { graph } from \"@pnp/graph\" ; const delSubscription = await graph . subscription . getById ( 'subscriptionId' ). delete (); Update a Subscription \u00b6 Using the subscriptions.getById().update() you can update one of the Subscriptions import { graph } from \"@pnp/graph\" ; const updSubscription = await graph . subscriptions . getById ( 'subscriptionId' ). update ({ changeType : \"created,updated,deleted\" });","title":"subscriptions"},{"location":"graph/docs/subscriptions/#pnpgraphsubscriptions","text":"The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: Mail, events, and contacts from Outlook. Conversations from Office Groups. Drive root items from OneDrive. Users and Groups from Azure Active Directory. * Alerts from the Microsoft Graph Security API.","title":"@pnp/graph/subscriptions"},{"location":"graph/docs/subscriptions/#get-all-of-the-subscriptions","text":"Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. import { graph } from \"@pnp/graph\" ; const subscriptions = await graph . subscriptions . get ();","title":"Get all of the Subscriptions"},{"location":"graph/docs/subscriptions/#create-a-new-subscription","text":"Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url. import { graph } from \"@pnp/graph\" ; const addedSubscription = await graph . subscriptions . add ( \"created,updated\" , \"https://webhook.azurewebsites.net/api/send/myNotifyClient\" , \"me/mailFolders('Inbox')/messages\" , \"2019-11-20T18:23:45.9356913Z\" );","title":"Create a new Subscription"},{"location":"graph/docs/subscriptions/#get-subscription-by-id","text":"Using the subscriptions.getById() you can get one of the subscriptions import { graph } from \"@pnp/graph\" ; const subscription = await graph . subscriptions . getById ( 'subscriptionId' );","title":"Get Subscription by Id"},{"location":"graph/docs/subscriptions/#delete-a-subscription","text":"Using the subscriptions.getById().delete() you can remove one of the Subscriptions import { graph } from \"@pnp/graph\" ; const delSubscription = await graph . subscription . getById ( 'subscriptionId' ). delete ();","title":"Delete a Subscription"},{"location":"graph/docs/subscriptions/#update-a-subscription","text":"Using the subscriptions.getById().update() you can update one of the Subscriptions import { graph } from \"@pnp/graph\" ; const updSubscription = await graph . subscriptions . getById ( 'subscriptionId' ). update ({ changeType : \"created,updated,deleted\" });","title":"Update a Subscription"},{"location":"graph/docs/teams/","text":"@pnp/graph/teams \u00b6 The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams. Teams the user is a member of \u00b6 import { graph } from \"@pnp/graph\" ; const joinedTeams = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). joinedTeams . get (); const myJoinedTeams = await graph . me . joinedTeams . get (); Get Teams by Id \u00b6 Using the teams.getById() you can get a specific Team. import { graph } from \"@pnp/graph\" ; const team = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). get (); Create new Group and Team \u00b6 When you create a new group and add a Team, the group needs to have an Owner. Or else we get an error. So the owner Id is important, and you could just get the users Ids from import { graph } from \"@pnp/graph\" ; const users = await graph . users . get (); Then create import { graph } from \"@pnp/graph\" ; const createdGroupTeam = await graph . teams . create ( 'Groupname' , 'mailNickname' , 'description' , 'OwnerId' ,{ \"memberSettings\" : { \"allowCreateUpdateChannels\" : true }, \"messagingSettings\" : { \"allowUserEditMessages\" : true , \"allowUserDeleteMessages\" : true }, \"funSettings\" : { \"allowGiphy\" : true , \"giphyContentRating\" : \"strict\" }}); Create a Team via a specific group \u00b6 Here we get the group via id and use createTeam import { graph } from \"@pnp/graph\" ; const createdTeam = await graph . groups . getById ( '679c8ff4-f07d-40de-b02b-60ec332472dd' ). createTeam ({ \"memberSettings\" : { \"allowCreateUpdateChannels\" : true }, \"messagingSettings\" : { \"allowUserEditMessages\" : true , \"allowUserDeleteMessages\" : true }, \"funSettings\" : { \"allowGiphy\" : true , \"giphyContentRating\" : \"strict\" }}); Archive a Team \u00b6 import { graph } from \"@pnp/graph\" ; const archived = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). archive (); Unarchive a Team \u00b6 import { graph } from \"@pnp/graph\" ; const archived = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). unarchive (); Clone a Team \u00b6 import { graph } from \"@pnp/graph\" ; const clonedTeam = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). cloneTeam ( 'Cloned' , 'mailNickname' , 'description' , 'apps,tabs,settings,channels,members' , 'public' ); Get all channels of a Team \u00b6 import { graph } from \"@pnp/graph\" ; const channels = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . get (); Get channel by Id \u00b6 import { graph } from \"@pnp/graph\" ; const channel = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). get (); Create a new Channel \u00b6 import { graph } from \"@pnp/graph\" ; const newChannel = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . create ( 'New Channel' , 'Description' ); Get installed Apps \u00b6 import { graph } from \"@pnp/graph\" ; const installedApps = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). installedApps . get (); Add an App \u00b6 import { graph } from \"@pnp/graph\" ; const addedApp = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). installedApps . add ( 'https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a' ); Remove an App \u00b6 import { graph } from \"@pnp/graph\" ; const removedApp = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). installedApps . remove (); Get Tabs from a Channel \u00b6 import { graph } from \"@pnp/graph\" ; const tabs = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). tabs . get (); Get Tab by Id \u00b6 import { graph } from \"@pnp/graph\" ; const tab = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). tabs . getById ( 'Id' ); Add a new Tab \u00b6 import { graph } from \"@pnp/graph\" ; const newTab = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). tabs . add ( 'Tab' , 'https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a' , < TabsConfiguration > {});","title":"teams"},{"location":"graph/docs/teams/#pnpgraphteams","text":"The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams.","title":"@pnp/graph/teams"},{"location":"graph/docs/teams/#teams-the-user-is-a-member-of","text":"import { graph } from \"@pnp/graph\" ; const joinedTeams = await graph . users . getById ( '99dc1039-eb80-43b1-a09e-250d50a80b26' ). joinedTeams . get (); const myJoinedTeams = await graph . me . joinedTeams . get ();","title":"Teams the user is a member of"},{"location":"graph/docs/teams/#get-teams-by-id","text":"Using the teams.getById() you can get a specific Team. import { graph } from \"@pnp/graph\" ; const team = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). get ();","title":"Get Teams by Id"},{"location":"graph/docs/teams/#create-new-group-and-team","text":"When you create a new group and add a Team, the group needs to have an Owner. Or else we get an error. So the owner Id is important, and you could just get the users Ids from import { graph } from \"@pnp/graph\" ; const users = await graph . users . get (); Then create import { graph } from \"@pnp/graph\" ; const createdGroupTeam = await graph . teams . create ( 'Groupname' , 'mailNickname' , 'description' , 'OwnerId' ,{ \"memberSettings\" : { \"allowCreateUpdateChannels\" : true }, \"messagingSettings\" : { \"allowUserEditMessages\" : true , \"allowUserDeleteMessages\" : true }, \"funSettings\" : { \"allowGiphy\" : true , \"giphyContentRating\" : \"strict\" }});","title":"Create new Group and Team"},{"location":"graph/docs/teams/#create-a-team-via-a-specific-group","text":"Here we get the group via id and use createTeam import { graph } from \"@pnp/graph\" ; const createdTeam = await graph . groups . getById ( '679c8ff4-f07d-40de-b02b-60ec332472dd' ). createTeam ({ \"memberSettings\" : { \"allowCreateUpdateChannels\" : true }, \"messagingSettings\" : { \"allowUserEditMessages\" : true , \"allowUserDeleteMessages\" : true }, \"funSettings\" : { \"allowGiphy\" : true , \"giphyContentRating\" : \"strict\" }});","title":"Create a Team via a specific group"},{"location":"graph/docs/teams/#archive-a-team","text":"import { graph } from \"@pnp/graph\" ; const archived = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). archive ();","title":"Archive a Team"},{"location":"graph/docs/teams/#unarchive-a-team","text":"import { graph } from \"@pnp/graph\" ; const archived = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). unarchive ();","title":"Unarchive a Team"},{"location":"graph/docs/teams/#clone-a-team","text":"import { graph } from \"@pnp/graph\" ; const clonedTeam = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). cloneTeam ( 'Cloned' , 'mailNickname' , 'description' , 'apps,tabs,settings,channels,members' , 'public' );","title":"Clone a Team"},{"location":"graph/docs/teams/#get-all-channels-of-a-team","text":"import { graph } from \"@pnp/graph\" ; const channels = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . get ();","title":"Get all channels of a Team"},{"location":"graph/docs/teams/#get-channel-by-id","text":"import { graph } from \"@pnp/graph\" ; const channel = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). get ();","title":"Get channel by Id"},{"location":"graph/docs/teams/#create-a-new-channel","text":"import { graph } from \"@pnp/graph\" ; const newChannel = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . create ( 'New Channel' , 'Description' );","title":"Create a new Channel"},{"location":"graph/docs/teams/#get-installed-apps","text":"import { graph } from \"@pnp/graph\" ; const installedApps = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). installedApps . get ();","title":"Get installed Apps"},{"location":"graph/docs/teams/#add-an-app","text":"import { graph } from \"@pnp/graph\" ; const addedApp = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). installedApps . add ( 'https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a' );","title":"Add an App"},{"location":"graph/docs/teams/#remove-an-app","text":"import { graph } from \"@pnp/graph\" ; const removedApp = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). installedApps . remove ();","title":"Remove an App"},{"location":"graph/docs/teams/#get-tabs-from-a-channel","text":"import { graph } from \"@pnp/graph\" ; const tabs = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). tabs . get ();","title":"Get Tabs from a Channel"},{"location":"graph/docs/teams/#get-tab-by-id","text":"import { graph } from \"@pnp/graph\" ; const tab = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). tabs . getById ( 'Id' );","title":"Get Tab by Id"},{"location":"graph/docs/teams/#add-a-new-tab","text":"import { graph } from \"@pnp/graph\" ; const newTab = await graph . teams . getById ( '3531f3fb-f9ee-4f43-982a-6c90d8226528' ). channels . getById ( '19:65723d632b384ca89c81115c281428a3@thread.skype' ). tabs . add ( 'Tab' , 'https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a' , < TabsConfiguration > {});","title":"Add a new Tab"},{"location":"logging/docs/","text":"@pnp/logging \u00b6 The logging module provides light weight subscribable and extensiable logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers. Getting Started \u00b6 Install the logging module, it has no other dependencies npm install @pnp/logging --save Understanding the Logging Framework \u00b6 The logging framework is based on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the LogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface. /** * Interface that defines a log listener * */ export interface LogListener { /** * Any associated data that a given logging listener may choose to log or ignore * * @param entry The information to be logged */ log ( entry : LogEntry ) : void ; } /** * Interface that defines a log entry * */ export interface LogEntry { /** * The main message to be logged */ message : string ; /** * The level of information this message represents */ level : LogLevel ; /** * Any associated data that a given logging listener may choose to log or ignore */ data? : any ; } Log Levels \u00b6 export const enum LogLevel { Verbose = 0 , Info = 1 , Warning = 2 , Error = 3 , Off = 99 , } Writing to the Logger \u00b6 To write information to a logger you can use either write, writeJSON, or log. import { Logger , LogLevel } from \"@pnp/logging\" ; // write logs a simple string as the message value of the LogEntry Logger . write ( \"This is logging a simple string\" ); // optionally passing a level, default level is Verbose Logger . write ( \"This is logging a simple string\" , LogLevel . Error ); // this will convert the object to a string using JSON.stringify and set the message with the result Logger . writeJSON ({ name : \"value\" , name2 : \"value2\" }); // optionally passing a level, default level is Verbose Logger . writeJSON ({ name : \"value\" , name2 : \"value2\" }, LogLevel . Warn ); // specify the entire LogEntry interface using log Logger . log ({ data : { name : \"value\" , name2 : \"value2\" }, level : LogLevel.Warning , message : \"This is my message\" }); Log an error \u00b6 There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance pased in, the level will be Error, and the message will be the Error instance message. const e = new Error ( \"An Error\" ); Logger . error ( e ); Subscribing a Listener \u00b6 By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. import { Logger , ConsoleListener , LogLevel } from \"@pnp/logging\" ; // subscribe a listener Logger . subscribe ( new ConsoleListener ()); // set the active log level Logger . activeLogLevel = LogLevel . Info ; Available Listeners \u00b6 There are two listeners included in the library, ConsoleListener and FunctionListener. ConsoleListener \u00b6 This listener outputs information to the console and works in Node as well as within browsers. It takes no settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Usage is shown in the example above. FunctionListener \u00b6 The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. import { Logger , FunctionListener , LogEntry } from \"@pnp/logging\" ; let listener = new FunctionListener (( entry : LogEntry ) => { // pass all logging data to an existing framework MyExistingCompanyLoggingFramework . log ( entry . message ); }); Logger . subscribe ( listener ); Create a Custom Listener \u00b6 If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the LogListener interface. import { Logger , LogListener , LogEntry } from \"@pnp/logging\" ; class MyListener implements LogListener { log ( entry : LogEntry ) : void { // here you would do something with the entry } } Logger . subscribe ( new MyListener ()); UML \u00b6 Graphical UML diagram of @pnp/logging. Right-click the diagram and open in new tab if it is too small.","title":"logging"},{"location":"logging/docs/#pnplogging","text":"The logging module provides light weight subscribable and extensiable logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.","title":"@pnp/logging"},{"location":"logging/docs/#getting-started","text":"Install the logging module, it has no other dependencies npm install @pnp/logging --save","title":"Getting Started"},{"location":"logging/docs/#understanding-the-logging-framework","text":"The logging framework is based on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the LogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface. /** * Interface that defines a log listener * */ export interface LogListener { /** * Any associated data that a given logging listener may choose to log or ignore * * @param entry The information to be logged */ log ( entry : LogEntry ) : void ; } /** * Interface that defines a log entry * */ export interface LogEntry { /** * The main message to be logged */ message : string ; /** * The level of information this message represents */ level : LogLevel ; /** * Any associated data that a given logging listener may choose to log or ignore */ data? : any ; }","title":"Understanding the Logging Framework"},{"location":"logging/docs/#log-levels","text":"export const enum LogLevel { Verbose = 0 , Info = 1 , Warning = 2 , Error = 3 , Off = 99 , }","title":"Log Levels"},{"location":"logging/docs/#writing-to-the-logger","text":"To write information to a logger you can use either write, writeJSON, or log. import { Logger , LogLevel } from \"@pnp/logging\" ; // write logs a simple string as the message value of the LogEntry Logger . write ( \"This is logging a simple string\" ); // optionally passing a level, default level is Verbose Logger . write ( \"This is logging a simple string\" , LogLevel . Error ); // this will convert the object to a string using JSON.stringify and set the message with the result Logger . writeJSON ({ name : \"value\" , name2 : \"value2\" }); // optionally passing a level, default level is Verbose Logger . writeJSON ({ name : \"value\" , name2 : \"value2\" }, LogLevel . Warn ); // specify the entire LogEntry interface using log Logger . log ({ data : { name : \"value\" , name2 : \"value2\" }, level : LogLevel.Warning , message : \"This is my message\" });","title":"Writing to the Logger"},{"location":"logging/docs/#log-an-error","text":"There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance pased in, the level will be Error, and the message will be the Error instance message. const e = new Error ( \"An Error\" ); Logger . error ( e );","title":"Log an error"},{"location":"logging/docs/#subscribing-a-listener","text":"By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. import { Logger , ConsoleListener , LogLevel } from \"@pnp/logging\" ; // subscribe a listener Logger . subscribe ( new ConsoleListener ()); // set the active log level Logger . activeLogLevel = LogLevel . Info ;","title":"Subscribing a Listener"},{"location":"logging/docs/#available-listeners","text":"There are two listeners included in the library, ConsoleListener and FunctionListener.","title":"Available Listeners"},{"location":"logging/docs/#consolelistener","text":"This listener outputs information to the console and works in Node as well as within browsers. It takes no settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Usage is shown in the example above.","title":"ConsoleListener"},{"location":"logging/docs/#functionlistener","text":"The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. import { Logger , FunctionListener , LogEntry } from \"@pnp/logging\" ; let listener = new FunctionListener (( entry : LogEntry ) => { // pass all logging data to an existing framework MyExistingCompanyLoggingFramework . log ( entry . message ); }); Logger . subscribe ( listener );","title":"FunctionListener"},{"location":"logging/docs/#create-a-custom-listener","text":"If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the LogListener interface. import { Logger , LogListener , LogEntry } from \"@pnp/logging\" ; class MyListener implements LogListener { log ( entry : LogEntry ) : void { // here you would do something with the entry } } Logger . subscribe ( new MyListener ());","title":"Create a Custom Listener"},{"location":"logging/docs/#uml","text":"Graphical UML diagram of @pnp/logging. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"nodejs/docs/","text":"@pnp/nodejs \u00b6 This package supplies helper code when using the @pnp libraries within the context of nodejs. This removes the node specific functionality from any of the packages. Primarily these consist of clients to enable use of the libraries in nodejs. Getting Started \u00b6 Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the exported functionality. npm install @pnp/logging @pnp/core @pnp/nodejs --save AdalFetchClient SPFetchClient BearerTokenFetchClient Using A Proxy UML \u00b6 Graphical UML diagram of @pnp/nodejs. Right-click the diagram and open in new tab if it is too small.","title":"nodejs"},{"location":"nodejs/docs/#pnpnodejs","text":"This package supplies helper code when using the @pnp libraries within the context of nodejs. This removes the node specific functionality from any of the packages. Primarily these consist of clients to enable use of the libraries in nodejs.","title":"@pnp/nodejs"},{"location":"nodejs/docs/#getting-started","text":"Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the exported functionality. npm install @pnp/logging @pnp/core @pnp/nodejs --save AdalFetchClient SPFetchClient BearerTokenFetchClient Using A Proxy","title":"Getting Started"},{"location":"nodejs/docs/#uml","text":"Graphical UML diagram of @pnp/nodejs. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"nodejs/docs/adal-certificate-fetch-client/","text":"@pnp/nodejs/adalcertificatefetchclient \u00b6 The AdalCertificateFetchClient class depends on the adal-node package to authenticate against Azure AD using the client credentials with a client certificate flow. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalCertificateFetchClient } from \"@pnp/nodejs\" ; import { graph } from \"@pnp/graph\" ; import * as fs from \"fs\" ; import * as path from \"path\" ; // Get the private key from a file (Assuming it's a .pem file) const keyPemFile = \"/path/to/privatekey.pem\" ; const privateKey = fs . readFileSync ( path . resolve ( __dirname , keyPemFile ), { encoding : 'utf8' } ); // setup the client using graph setup function graph . setup ({ graph : { fetchClientFactory : () => { return new AdalCertificateFetchClient ( \"{tenant id}\" , \"{app id}\" , \"{certificate thumbprint}\" , privateKey ); }, }, }); // execute a library request as normal graph . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"@pnp/nodejs/adalcertificatefetchclient"},{"location":"nodejs/docs/adal-certificate-fetch-client/#pnpnodejsadalcertificatefetchclient","text":"The AdalCertificateFetchClient class depends on the adal-node package to authenticate against Azure AD using the client credentials with a client certificate flow. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalCertificateFetchClient } from \"@pnp/nodejs\" ; import { graph } from \"@pnp/graph\" ; import * as fs from \"fs\" ; import * as path from \"path\" ; // Get the private key from a file (Assuming it's a .pem file) const keyPemFile = \"/path/to/privatekey.pem\" ; const privateKey = fs . readFileSync ( path . resolve ( __dirname , keyPemFile ), { encoding : 'utf8' } ); // setup the client using graph setup function graph . setup ({ graph : { fetchClientFactory : () => { return new AdalCertificateFetchClient ( \"{tenant id}\" , \"{app id}\" , \"{certificate thumbprint}\" , privateKey ); }, }, }); // execute a library request as normal graph . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"@pnp/nodejs/adalcertificatefetchclient"},{"location":"nodejs/docs/adal-fetch-client/","text":"@pnp/nodejs/adalfetchclient \u00b6 The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalFetchClient } from \"@pnp/nodejs\" ; import { graph } from \"@pnp/graph\" ; // setup the client using graph setup function graph . setup ({ graph : { fetchClientFactory : () => { return new AdalFetchClient ( \"{tenant}\" , \"{app id}\" , \"{app secret}\" ); }, }, }); // execute a library request as normal graph . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"AdalFetchClient"},{"location":"nodejs/docs/adal-fetch-client/#pnpnodejsadalfetchclient","text":"The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalFetchClient } from \"@pnp/nodejs\" ; import { graph } from \"@pnp/graph\" ; // setup the client using graph setup function graph . setup ({ graph : { fetchClientFactory : () => { return new AdalFetchClient ( \"{tenant}\" , \"{app id}\" , \"{app secret}\" ); }, }, }); // execute a library request as normal graph . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"@pnp/nodejs/adalfetchclient"},{"location":"nodejs/docs/bearer-token-fetch-client/","text":"@pnp/nodejs/BearerTokenFetchClient \u00b6 The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. import { BearerTokenFetchClient } from \"@pnp/nodejs\" ; import { graph } from \"@pnp/graph\" ; // setup the client using graph setup function graph . setup ({ graph : { fetchClientFactory : () => { return new BearerTokenFetchClient ( \"{Bearer Token}\" ); }, }, }); // execute a library request as normal graph . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"BearerTokenFetchClient"},{"location":"nodejs/docs/bearer-token-fetch-client/#pnpnodejsbearertokenfetchclient","text":"The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. import { BearerTokenFetchClient } from \"@pnp/nodejs\" ; import { graph } from \"@pnp/graph\" ; // setup the client using graph setup function graph . setup ({ graph : { fetchClientFactory : () => { return new BearerTokenFetchClient ( \"{Bearer Token}\" ); }, }, }); // execute a library request as normal graph . groups . get (). then ( g => { console . log ( JSON . stringify ( g , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"@pnp/nodejs/BearerTokenFetchClient"},{"location":"nodejs/docs/provider-hosted-app/","text":"@pnp/nodejs/providerhostedrequestcontext \u00b6 Added in 1.2.7 The ProviderHostedRequestcontext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. Note : To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context. import { sp , SPRest } from \"@pnp/sp\" ; import { NodeFetchClient , ProviderHostedRequestContext } from \"@pnp/nodejs\" ; // configure your node options sp . setup ({ sp : { fetchClientFactory : () => { return new NodeFetchClient (); }, }, }); // get request data generated by /_layouts/15/AppRedirect.aspx const spAppToken = request . body . SPAppToken ; const spSiteUrl = request . body . SPSiteUrl ; // create a context based on the add-in details and SPAppToken const ctx = await ProviderHostedRequestContext . create ( spSiteUrl , \"{client id}\" , \"{client secret}\" , spAppToken ); // create an SPRest object configured to use our context // this is used in place of the global sp object const userSP = new SPRest (). configure ( await ctx . getUserConfig (), spSiteUrl ); const addinSP = new SPRest (). configure ( await ctx . getAddInOnlyConfig (), spSiteUrl ); // make a request on behalf of the user const user = await userSP . web . currentUser . get (); console . log ( `Hello ${ user . Title } ` ); // make an add-in only request const app = await addinSP . web . currentUser . get (); console . log ( `Add-in principal: ${ app . Title } ` );","title":"ProviderHostedRequestContext"},{"location":"nodejs/docs/provider-hosted-app/#pnpnodejsproviderhostedrequestcontext","text":"Added in 1.2.7 The ProviderHostedRequestcontext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. Note : To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context. import { sp , SPRest } from \"@pnp/sp\" ; import { NodeFetchClient , ProviderHostedRequestContext } from \"@pnp/nodejs\" ; // configure your node options sp . setup ({ sp : { fetchClientFactory : () => { return new NodeFetchClient (); }, }, }); // get request data generated by /_layouts/15/AppRedirect.aspx const spAppToken = request . body . SPAppToken ; const spSiteUrl = request . body . SPSiteUrl ; // create a context based on the add-in details and SPAppToken const ctx = await ProviderHostedRequestContext . create ( spSiteUrl , \"{client id}\" , \"{client secret}\" , spAppToken ); // create an SPRest object configured to use our context // this is used in place of the global sp object const userSP = new SPRest (). configure ( await ctx . getUserConfig (), spSiteUrl ); const addinSP = new SPRest (). configure ( await ctx . getAddInOnlyConfig (), spSiteUrl ); // make a request on behalf of the user const user = await userSP . web . currentUser . get (); console . log ( `Hello ${ user . Title } ` ); // make an add-in only request const app = await addinSP . web . currentUser . get (); console . log ( `Add-in principal: ${ app . Title } ` );","title":"@pnp/nodejs/providerhostedrequestcontext"},{"location":"nodejs/docs/proxy/","text":"@pnp/nodejs/proxy \u00b6 Added in 1.3.2 In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler. In the 1.3.2 relesae we introduced the ability to use a proxy with the @pnp/nodejs library. Basic Usage \u00b6 You need to import the new setProxyUrl function from the library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library. import { SPFetchClient , SPOAuthEnv , setProxyUrl } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { // call the set proxy url function and it will be used for all requests regardless of client setProxyUrl ( \"{your proxy url}\" ); return new SPFetchClient ( settings . testing . sp . url , settings . testing . sp . id , settings . testing . sp . secret , SPOAuthEnv . SPO ); }, }, }); Use with Fiddler \u00b6 To get Fiddler to work you may need to set an environment variable. This should only be done for testing! import { SPFetchClient , SPOAuthEnv , setProxyUrl } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { // ignore certificate errors: ONLY FOR TESTING!! process . env . NODE_TLS_REJECT_UNAUTHORIZED = \"0\" ; // this is my fiddler url locally setProxyUrl ( \"http://127.0.0.1:8888\" ); return new SPFetchClient ( settings . testing . sp . url , settings . testing . sp . id , settings . testing . sp . secret , SPOAuthEnv . SPO ); }, }, });","title":"@pnp/nodejs/proxy"},{"location":"nodejs/docs/proxy/#pnpnodejsproxy","text":"Added in 1.3.2 In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler. In the 1.3.2 relesae we introduced the ability to use a proxy with the @pnp/nodejs library.","title":"@pnp/nodejs/proxy"},{"location":"nodejs/docs/proxy/#basic-usage","text":"You need to import the new setProxyUrl function from the library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library. import { SPFetchClient , SPOAuthEnv , setProxyUrl } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { // call the set proxy url function and it will be used for all requests regardless of client setProxyUrl ( \"{your proxy url}\" ); return new SPFetchClient ( settings . testing . sp . url , settings . testing . sp . id , settings . testing . sp . secret , SPOAuthEnv . SPO ); }, }, });","title":"Basic Usage"},{"location":"nodejs/docs/proxy/#use-with-fiddler","text":"To get Fiddler to work you may need to set an environment variable. This should only be done for testing! import { SPFetchClient , SPOAuthEnv , setProxyUrl } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { // ignore certificate errors: ONLY FOR TESTING!! process . env . NODE_TLS_REJECT_UNAUTHORIZED = \"0\" ; // this is my fiddler url locally setProxyUrl ( \"http://127.0.0.1:8888\" ); return new SPFetchClient ( settings . testing . sp . url , settings . testing . sp . id , settings . testing . sp . secret , SPOAuthEnv . SPO ); }, }, });","title":"Use with Fiddler"},{"location":"nodejs/docs/sp-fetch-client/","text":"@pnp/nodejs/spfetchclient \u00b6 The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. import { SPFetchClient } from \"@pnp/nodejs\" ; import { sp } from \"@pnp/sp\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" ); }, }, }); // execute a library request as normal sp . web . get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }). catch ( e => { console . error ( e ); }); Set Authentication Environment \u00b6 Added in 1.1.2 For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. SPO : (default) for all *.sharepoint.com urls China: for China hosted cloud Germany: for Germany local cloud USDef: USA Defense cloud USGov: USA Government cloud import { sp } from \"@pnp/sp\" ; import { SPFetchClient , SPOAuthEnv } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" , SPOAuthEnv . China ); }, }, }); Set Realm \u00b6 In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to \"https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx\" and copying the GUID value that appears after the \"@\" - this is the realm id. As of version 1.1.2 the realm parameter is now the 5th parameter in the constructor. import { sp } from \"@pnp/sp\" ; import { SPFetchClient , SPOAuthEnv } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" , SPOAuthEnv . SPO , \"{realm}\" ); }, }, }); Creating a client id and secret \u00b6 This section outlines how to register for a client id and secret for use in the above code. Register An Add-In \u00b6 Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri, you can use the values shown in the examples Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article. Grant Your Add-In Permissions \u00b6 Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the Add Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note that the above XML will grant full tenant control, you should grant only those permissions necessary for your application","title":"SPFetchClient"},{"location":"nodejs/docs/sp-fetch-client/#pnpnodejsspfetchclient","text":"The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. import { SPFetchClient } from \"@pnp/nodejs\" ; import { sp } from \"@pnp/sp\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" ); }, }, }); // execute a library request as normal sp . web . get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }). catch ( e => { console . error ( e ); });","title":"@pnp/nodejs/spfetchclient"},{"location":"nodejs/docs/sp-fetch-client/#set-authentication-environment","text":"Added in 1.1.2 For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. SPO : (default) for all *.sharepoint.com urls China: for China hosted cloud Germany: for Germany local cloud USDef: USA Defense cloud USGov: USA Government cloud import { sp } from \"@pnp/sp\" ; import { SPFetchClient , SPOAuthEnv } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" , SPOAuthEnv . China ); }, }, });","title":"Set Authentication Environment"},{"location":"nodejs/docs/sp-fetch-client/#set-realm","text":"In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to \"https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx\" and copying the GUID value that appears after the \"@\" - this is the realm id. As of version 1.1.2 the realm parameter is now the 5th parameter in the constructor. import { sp } from \"@pnp/sp\" ; import { SPFetchClient , SPOAuthEnv } from \"@pnp/nodejs\" ; sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{site url}\" , \"{client id}\" , \"{client secret}\" , SPOAuthEnv . SPO , \"{realm}\" ); }, }, });","title":"Set Realm"},{"location":"nodejs/docs/sp-fetch-client/#creating-a-client-id-and-secret","text":"This section outlines how to register for a client id and secret for use in the above code.","title":"Creating a client id and secret"},{"location":"nodejs/docs/sp-fetch-client/#register-an-add-in","text":"Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri, you can use the values shown in the examples Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.","title":"Register An Add-In"},{"location":"nodejs/docs/sp-fetch-client/#grant-your-add-in-permissions","text":"Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the Add Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note that the above XML will grant full tenant control, you should grant only those permissions necessary for your application","title":"Grant Your Add-In Permissions"},{"location":"odata/docs/","text":"@pnp/queryable \u00b6 This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure the core code is solid and well tested, with any updates benefitting all inheriting libraries. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable --save Library Topics \u00b6 caching core OData Batching Parsers Pipeline Queryable UML \u00b6 Graphical UML diagram of @pnp/queryable. Right-click the diagram and open in new tab if it is too small.","title":"odata"},{"location":"odata/docs/#pnpodata","text":"This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure the core code is solid and well tested, with any updates benefitting all inheriting libraries.","title":"@pnp/queryable"},{"location":"odata/docs/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable --save","title":"Getting Started"},{"location":"odata/docs/#library-topics","text":"caching core OData Batching Parsers Pipeline Queryable","title":"Library Topics"},{"location":"odata/docs/#uml","text":"Graphical UML diagram of @pnp/queryable. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"odata/docs/caching/","text":"@pnp/queryable/caching \u00b6 Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph. Basic example \u00b6 You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The below code will get the items from the list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() should always be the last method in the chain before the get() (OR if you are using [[batching]] these methods can be transposed, more details below). import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"Tasks\" ). items . usingCaching (). get (). then ( r => { console . log ( r ) }); sp . web . lists . getByTitle ( \"Tasks\" ). items . top ( 5 ). orderBy ( \"Modified\" ). usingCaching (). get (). then ( r => { console . log ( r ) }); Globally Configure Cache Settings \u00b6 If you would like to not use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. import { sp } from \"@pnp/sp\" ; sp . setup ({ defaultCachingStore : \"session\" , // or \"local\" defaultCachingTimeoutSeconds : 30 , globalCacheDisable : false // or true to disable caching in case of debugging/testing }); sp . web . lists . getByTitle ( \"Tasks\" ). items . top ( 5 ). orderBy ( \"Modified\" ). usingCaching (). get (). then ( r => { console . log ( r ) }); Per Call Configuration \u00b6 If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. export interface ICachingOptions { expiration? : Date ; storeName ?: \"session\" | \"local\" ; key : string ; } import { sp } from \"@pnp/sp\" ; import { dateAdd } from \"@pnp/core\" ; sp . web . lists . getByTitle ( \"Tasks\" ). items . top ( 5 ). orderBy ( \"Modified\" ). usingCaching ({ expiration : dateAdd ( new Date (), \"minute\" , 20 ), key : \"My Key\" , storeName : \"local\" }). get (). then ( r => { console . log ( r ) }); Using Batching with Caching \u00b6 You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. import { sp } from \"@pnp/sp\" ; let batch = sp . createBatch (); sp . web . lists . inBatch ( batch ). usingCaching (). get (). then ( r => { console . log ( r ) }); sp . web . lists . getByTitle ( \"Tasks\" ). items . usingCaching (). inBatch ( batch ). get (). then ( r => { console . log ( r ) }); batch . execute (). then (() => console . log ( \"All done!\" )); Implement Custom Caching \u00b6 You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here . Implement caching helper method: \u00b6 We create a map to act as our cache storage and a function to wrap the request caching logic const map = new Map < string , any > (); async function staleWhileRevalidate < T > ( key : string , p : Promise < T > ) : Promise < T > { if ( map . has ( key )) { // In Cache p . then ( u => { // Update Cache once we have a result map . set ( key , u ); }); // Return from Cache return map . get ( key ); } // Not In Cache so we need to wait for the value const r = await p ; // Set Cache map . set ( key , r ); // Return from Promise return r ; } Usage \u00b6 Don't call usingCaching just apply the helper method // this one will wait for the request to finish const r1 = await staleWhileRevalidate ( \"test1\" , sp . web . select ( \"Title\" , \"Description\" ). get ()); console . log ( JSON . stringify ( r1 , null , 2 )); // this one will return the result from cache and then update the cache in the background const r2 = await staleWhileRevalidate ( \"test1\" , sp . web . select ( \"Title\" , \"Description\" ). get ()); console . log ( JSON . stringify ( r2 , null , 2 )); Wrapper Function \u00b6 You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. interface WebData { Title : string ; Description : string ; } function getWebData () : Promise < WebData > { return staleWhileRevalidate ( \"test1\" , sp . web . select ( \"Title\" , \"Description\" ). get ()); } // this one will wait for the request to finish const r1 = await getWebData (); console . log ( JSON . stringify ( r1 , null , 2 )); // this one will return the result from cache and then update the cache in the background const r2 = await getWebData (); console . log ( JSON . stringify ( r2 , null , 2 ));","title":"caching"},{"location":"odata/docs/caching/#pnpodatacaching","text":"Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph.","title":"@pnp/queryable/caching"},{"location":"odata/docs/caching/#basic-example","text":"You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The below code will get the items from the list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() should always be the last method in the chain before the get() (OR if you are using [[batching]] these methods can be transposed, more details below). import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"Tasks\" ). items . usingCaching (). get (). then ( r => { console . log ( r ) }); sp . web . lists . getByTitle ( \"Tasks\" ). items . top ( 5 ). orderBy ( \"Modified\" ). usingCaching (). get (). then ( r => { console . log ( r ) });","title":"Basic example"},{"location":"odata/docs/caching/#globally-configure-cache-settings","text":"If you would like to not use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. import { sp } from \"@pnp/sp\" ; sp . setup ({ defaultCachingStore : \"session\" , // or \"local\" defaultCachingTimeoutSeconds : 30 , globalCacheDisable : false // or true to disable caching in case of debugging/testing }); sp . web . lists . getByTitle ( \"Tasks\" ). items . top ( 5 ). orderBy ( \"Modified\" ). usingCaching (). get (). then ( r => { console . log ( r ) });","title":"Globally Configure Cache Settings"},{"location":"odata/docs/caching/#per-call-configuration","text":"If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. export interface ICachingOptions { expiration? : Date ; storeName ?: \"session\" | \"local\" ; key : string ; } import { sp } from \"@pnp/sp\" ; import { dateAdd } from \"@pnp/core\" ; sp . web . lists . getByTitle ( \"Tasks\" ). items . top ( 5 ). orderBy ( \"Modified\" ). usingCaching ({ expiration : dateAdd ( new Date (), \"minute\" , 20 ), key : \"My Key\" , storeName : \"local\" }). get (). then ( r => { console . log ( r ) });","title":"Per Call Configuration"},{"location":"odata/docs/caching/#using-batching-with-caching","text":"You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. import { sp } from \"@pnp/sp\" ; let batch = sp . createBatch (); sp . web . lists . inBatch ( batch ). usingCaching (). get (). then ( r => { console . log ( r ) }); sp . web . lists . getByTitle ( \"Tasks\" ). items . usingCaching (). inBatch ( batch ). get (). then ( r => { console . log ( r ) }); batch . execute (). then (() => console . log ( \"All done!\" ));","title":"Using Batching with Caching"},{"location":"odata/docs/caching/#implement-custom-caching","text":"You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here .","title":"Implement Custom Caching"},{"location":"odata/docs/caching/#implement-caching-helper-method","text":"We create a map to act as our cache storage and a function to wrap the request caching logic const map = new Map < string , any > (); async function staleWhileRevalidate < T > ( key : string , p : Promise < T > ) : Promise < T > { if ( map . has ( key )) { // In Cache p . then ( u => { // Update Cache once we have a result map . set ( key , u ); }); // Return from Cache return map . get ( key ); } // Not In Cache so we need to wait for the value const r = await p ; // Set Cache map . set ( key , r ); // Return from Promise return r ; }","title":"Implement caching helper method:"},{"location":"odata/docs/caching/#usage","text":"Don't call usingCaching just apply the helper method // this one will wait for the request to finish const r1 = await staleWhileRevalidate ( \"test1\" , sp . web . select ( \"Title\" , \"Description\" ). get ()); console . log ( JSON . stringify ( r1 , null , 2 )); // this one will return the result from cache and then update the cache in the background const r2 = await staleWhileRevalidate ( \"test1\" , sp . web . select ( \"Title\" , \"Description\" ). get ()); console . log ( JSON . stringify ( r2 , null , 2 ));","title":"Usage"},{"location":"odata/docs/caching/#wrapper-function","text":"You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. interface WebData { Title : string ; Description : string ; } function getWebData () : Promise < WebData > { return staleWhileRevalidate ( \"test1\" , sp . web . select ( \"Title\" , \"Description\" ). get ()); } // this one will wait for the request to finish const r1 = await getWebData (); console . log ( JSON . stringify ( r1 , null , 2 )); // this one will return the result from cache and then update the cache in the background const r2 = await getWebData (); console . log ( JSON . stringify ( r2 , null , 2 ));","title":"Wrapper Function"},{"location":"odata/docs/core/","text":"@pnp/queryable/core \u00b6 This modules contains shared interfaces and abstract classes used within, and by inheritors of, the @pnp/queryable package. ProcessHttpClientResponseException \u00b6 The exception thrown when a response is returned and cannot be processed. interface ODataParser \u00b6 Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the value to be returned. It has two methods, one is optional: parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor ODataParserBase \u00b6 The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods. Create a custom parser from ODataParserBase \u00b6 You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases. class MyParser extends ODataParserBase < any > { // we need to override the parse method to do our custom stuff public parse ( r : Response ) : Promise < T > { // we wrap everything in a promise return new Promise (( resolve , reject ) => { // lets use the default error handling which returns true for no error // and will call reject with an error if one exists if ( this . handleError ( r , reject )) { // now we add our custom parsing here r . text (). then ( txt => { // here we call a madeup function to parse the result // this is where we would do our parsing as required myCustomerUnencode ( txt ). then ( v => { resolve ( v ); }); }); } }); } }","title":"core"},{"location":"odata/docs/core/#pnpodatacore","text":"This modules contains shared interfaces and abstract classes used within, and by inheritors of, the @pnp/queryable package.","title":"@pnp/queryable/core"},{"location":"odata/docs/core/#processhttpclientresponseexception","text":"The exception thrown when a response is returned and cannot be processed.","title":"ProcessHttpClientResponseException"},{"location":"odata/docs/core/#interface-odataparser","text":"Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the value to be returned. It has two methods, one is optional: parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor","title":"interface ODataParser"},{"location":"odata/docs/core/#odataparserbase","text":"The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods.","title":"ODataParserBase"},{"location":"odata/docs/core/#create-a-custom-parser-from-odataparserbase","text":"You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases. class MyParser extends ODataParserBase < any > { // we need to override the parse method to do our custom stuff public parse ( r : Response ) : Promise < T > { // we wrap everything in a promise return new Promise (( resolve , reject ) => { // lets use the default error handling which returns true for no error // and will call reject with an error if one exists if ( this . handleError ( r , reject )) { // now we add our custom parsing here r . text (). then ( txt => { // here we call a madeup function to parse the result // this is where we would do our parsing as required myCustomerUnencode ( txt ). then ( v => { resolve ( v ); }); }); } }); } }","title":"Create a custom parser from ODataParserBase"},{"location":"odata/docs/odata-batch/","text":"@pnp/queryable/odatabatch \u00b6 This module contains an abstract class used as a base when inheriting libraries support batching. ODataBatchRequestInfo \u00b6 This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will be responsible for processing that info by implementing the abstract executeImpl method. ODataBatch \u00b6 Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp and @pnp/graph modules.","title":"OData Batching"},{"location":"odata/docs/odata-batch/#pnpodataodatabatch","text":"This module contains an abstract class used as a base when inheriting libraries support batching.","title":"@pnp/queryable/odatabatch"},{"location":"odata/docs/odata-batch/#odatabatchrequestinfo","text":"This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will be responsible for processing that info by implementing the abstract executeImpl method.","title":"ODataBatchRequestInfo"},{"location":"odata/docs/odata-batch/#odatabatch","text":"Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp and @pnp/graph modules.","title":"ODataBatch"},{"location":"odata/docs/parsers/","text":"@pnp/queryable/parsers \u00b6 This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need. ODataDefaultParser \u00b6 The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. import { sp } from \"@pnp/sp\" ; import { JSONParser } from \"@pnp/queryable\" ; try { const parser = new JSONParser (); // this always throws a 404 error await sp . web . getList ( \"doesn't exist\" ). get ( parser ); } catch ( e ) { // we can check for the property \"isHttpRequestError\" to see if this is an instance of our class // this gets by all the many limitations of subclassing Error and type detection in JavaScript if ( e . hasOwnProperty ( \"isHttpRequestError\" )) { console . log ( \"e is HttpRequestError\" ); // now we can access the various properties and make use of the response object. // at this point the body is unread console . log ( `status: ${ e . status } ` ); console . log ( `statusText: ${ e . statusText } ` ); const json = await e . response . clone (). json (); console . log ( JSON . stringify ( json )); const text = await e . response . clone (). text (); console . log ( text ); const headers = e . response . headers ; } console . error ( e ); } TextParser \u00b6 Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files. BlobParser \u00b6 Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files. JSONParser \u00b6 Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files. BufferParser \u00b6 Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files. LambdaParser \u00b6 Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type. import { LambdaParser } from \"@pnp/queryable\" ; import { sp } from \"@pnp/sp\" ; // here a simple parser duplicating the functionality of the JSONParser const parser = new LambdaParser (( r : Response ) => r . json ()); const webDataJson = await sp . web . get ( parser ); console . log ( webDataJson );","title":"Parsers"},{"location":"odata/docs/parsers/#pnpodataparsers","text":"This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need.","title":"@pnp/queryable/parsers"},{"location":"odata/docs/parsers/#odatadefaultparser","text":"The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. import { sp } from \"@pnp/sp\" ; import { JSONParser } from \"@pnp/queryable\" ; try { const parser = new JSONParser (); // this always throws a 404 error await sp . web . getList ( \"doesn't exist\" ). get ( parser ); } catch ( e ) { // we can check for the property \"isHttpRequestError\" to see if this is an instance of our class // this gets by all the many limitations of subclassing Error and type detection in JavaScript if ( e . hasOwnProperty ( \"isHttpRequestError\" )) { console . log ( \"e is HttpRequestError\" ); // now we can access the various properties and make use of the response object. // at this point the body is unread console . log ( `status: ${ e . status } ` ); console . log ( `statusText: ${ e . statusText } ` ); const json = await e . response . clone (). json (); console . log ( JSON . stringify ( json )); const text = await e . response . clone (). text (); console . log ( text ); const headers = e . response . headers ; } console . error ( e ); }","title":"ODataDefaultParser"},{"location":"odata/docs/parsers/#textparser","text":"Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files.","title":"TextParser"},{"location":"odata/docs/parsers/#blobparser","text":"Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files.","title":"BlobParser"},{"location":"odata/docs/parsers/#jsonparser","text":"Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files.","title":"JSONParser"},{"location":"odata/docs/parsers/#bufferparser","text":"Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files.","title":"BufferParser"},{"location":"odata/docs/parsers/#lambdaparser","text":"Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type. import { LambdaParser } from \"@pnp/queryable\" ; import { sp } from \"@pnp/sp\" ; // here a simple parser duplicating the functionality of the JSONParser const parser = new LambdaParser (( r : Response ) => r . json ()); const webDataJson = await sp . web . get ( parser ); console . log ( webDataJson );","title":"LambdaParser"},{"location":"odata/docs/pipeline/","text":"@pnp/queryable/pipeline \u00b6 All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline. interface RequestContext \u00b6 The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required. interface RequestContext < T > { batch : ODataBatch ; batchDependency : () => void ; cachingOptions : ICachingOptions ; hasResult? : boolean ; isBatched : boolean ; isCached : boolean ; options : FetchOptions ; parser : ODataParser < T > ; pipeline : Array < ( c : RequestContext < T > ) => Promise < RequestContext < T >>> ; requestAbsoluteUrl : string ; requestId : string ; result? : T ; verb : string ; clientFactory : () => RequestClient ; } requestPipelineMethod decorator \u00b6 The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existance of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. @requestPipelineMethod ( true ) public static myPipelineMethod < T > ( context : RequestContext < T > ) : Promise < RequestContext < T >> { return new Promise < RequestContext < T >> ( resolve => { // do something resolve ( context ); }); } Default Pipeline \u00b6 logs the start of the request checks the cache for a value based on the context's cache settings sends the request if no value from found in the cache logs the end of the request","title":"Pipeline"},{"location":"odata/docs/pipeline/#pnpodatapipeline","text":"All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline.","title":"@pnp/queryable/pipeline"},{"location":"odata/docs/pipeline/#interface-requestcontext","text":"The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required. interface RequestContext < T > { batch : ODataBatch ; batchDependency : () => void ; cachingOptions : ICachingOptions ; hasResult? : boolean ; isBatched : boolean ; isCached : boolean ; options : FetchOptions ; parser : ODataParser < T > ; pipeline : Array < ( c : RequestContext < T > ) => Promise < RequestContext < T >>> ; requestAbsoluteUrl : string ; requestId : string ; result? : T ; verb : string ; clientFactory : () => RequestClient ; }","title":"interface RequestContext"},{"location":"odata/docs/pipeline/#requestpipelinemethod-decorator","text":"The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existance of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. @requestPipelineMethod ( true ) public static myPipelineMethod < T > ( context : RequestContext < T > ) : Promise < RequestContext < T >> { return new Promise < RequestContext < T >> ( resolve => { // do something resolve ( context ); }); }","title":"requestPipelineMethod decorator"},{"location":"odata/docs/pipeline/#default-pipeline","text":"logs the start of the request checks the cache for a value based on the context's cache settings sends the request if no value from found in the cache logs the end of the request","title":"Default Pipeline"},{"location":"odata/docs/queryable/","text":"@pnp/queryable/queryable \u00b6 The Queryable class is the base class for all of the libraries building fluent request apis. abstract class ODataQueryable \u00b6 This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls. properties \u00b6 query \u00b6 Provides access to the query string builder for this url public methods \u00b6 concat \u00b6 Directly concatenates the supplied string to the current url, not normalizing \"/\" chars configure \u00b6 Sets custom options for current object and all derived objects accessible via chaining import { ConfigOptions } from \"@pnp/queryable\" ; import { sp } from \"@pnp/sp\" ; const headers : ConfigOptions = { Accept : 'application/json;odata=nometadata' }; // here we use configure to set the headers value for all child requests of the list instance const list = sp . web . lists . getByTitle ( \"List1\" ). configure ({ headers }); // this will use the values set in configure list . items . get (). then ( items => console . log ( JSON . stringify ( items , null , 2 )); For reference the ConfigOptions interface is shown below: export interface ConfigOptions { headers? : string [][] | { [ key : string ] : string } | Headers ; mode ?: \"navigate\" | \"same-origin\" | \"no-cors\" | \"cors\" ; credentials ?: \"omit\" | \"same-origin\" | \"include\" ; cache ?: \"default\" | \"no-store\" | \"reload\" | \"no-cache\" | \"force-cache\" | \"only-if-cached\" ; } configureFrom \u00b6 Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance. usingCaching \u00b6 Enables caching for this request. See caching for more details. import { sp } from \"@pnp/sp\" sp . web . usingCaching (). get (). then (...); inBatch \u00b6 Adds this query to the supplied batch toUrl \u00b6 Gets the current url abstract toUrlAndQuery() \u00b6 When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request get \u00b6 Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.","title":"Queryable"},{"location":"odata/docs/queryable/#pnpodataqueryable","text":"The Queryable class is the base class for all of the libraries building fluent request apis.","title":"@pnp/queryable/queryable"},{"location":"odata/docs/queryable/#abstract-class-odataqueryable","text":"This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls.","title":"abstract class ODataQueryable"},{"location":"odata/docs/queryable/#properties","text":"","title":"properties"},{"location":"odata/docs/queryable/#query","text":"Provides access to the query string builder for this url","title":"query"},{"location":"odata/docs/queryable/#public-methods","text":"","title":"public methods"},{"location":"odata/docs/queryable/#concat","text":"Directly concatenates the supplied string to the current url, not normalizing \"/\" chars","title":"concat"},{"location":"odata/docs/queryable/#configure","text":"Sets custom options for current object and all derived objects accessible via chaining import { ConfigOptions } from \"@pnp/queryable\" ; import { sp } from \"@pnp/sp\" ; const headers : ConfigOptions = { Accept : 'application/json;odata=nometadata' }; // here we use configure to set the headers value for all child requests of the list instance const list = sp . web . lists . getByTitle ( \"List1\" ). configure ({ headers }); // this will use the values set in configure list . items . get (). then ( items => console . log ( JSON . stringify ( items , null , 2 )); For reference the ConfigOptions interface is shown below: export interface ConfigOptions { headers? : string [][] | { [ key : string ] : string } | Headers ; mode ?: \"navigate\" | \"same-origin\" | \"no-cors\" | \"cors\" ; credentials ?: \"omit\" | \"same-origin\" | \"include\" ; cache ?: \"default\" | \"no-store\" | \"reload\" | \"no-cache\" | \"force-cache\" | \"only-if-cached\" ; }","title":"configure"},{"location":"odata/docs/queryable/#configurefrom","text":"Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance.","title":"configureFrom"},{"location":"odata/docs/queryable/#usingcaching","text":"Enables caching for this request. See caching for more details. import { sp } from \"@pnp/sp\" sp . web . usingCaching (). get (). then (...);","title":"usingCaching"},{"location":"odata/docs/queryable/#inbatch","text":"Adds this query to the supplied batch","title":"inBatch"},{"location":"odata/docs/queryable/#tourl","text":"Gets the current url","title":"toUrl"},{"location":"odata/docs/queryable/#abstract-tourlandquery","text":"When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request","title":"abstract toUrlAndQuery()"},{"location":"odata/docs/queryable/#get","text":"Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.","title":"get"},{"location":"pnpjs/docs/","text":"@pnp/pnpjs \u00b6 The pnpjs library is a rollup of the core libraries across the @pnp scope and is designed only as a bridge to help folks transition from sp-pnp-js, primarily in scenarios where a single file is being imported via a script tag. It is recommended to not use this rollup library where possible and migrate to the individual libraries . Getting Started \u00b6 There are two approaches to using this library: the first is to import, the second is to manually extract the bundled file for use in your project. Install \u00b6 npm install @pnp/pnpjs --save You can then make use of the pnpjs rollup library within your application. It's structure matches sp-pnp-js, though some things may have changed based on the rolled-up dependencies. import pnp from \"@pnp/pnpjs\" ; pnp . sp . web . get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }); Grab Bundle File \u00b6 This method is useful if you are primarily working within a script editor web part or similar case where you are not using a build pipeline to bundle your application. Install only this library. npm install @pnp/pnpjs Browse to ./node_modules/@pnp/pnpjs/dist and grab either pnpjs.es5.umd.bundle.js or pnpjs.es5.umd.bundle.min.js depending on your needs. You can then add a script tag referencing this file and you will have a global variable \"pnp\". For example you could paste the following into a script editor web part: < p > Script Editor is on page. < script src = \"https://mysite/site_assets/pnpjs.es5.umd.bundle.min.js\" type = \"text/javascript\" > < script type = \"text/javascript\" > pnp . Logger . subscribe ( new pnp . ConsoleListener ()); pnp . Logger . activeLogLevel = pnp . LogLevel . Info ; pnp . sp . web . get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }); Alternatively to serve the script from the project at \"https://localhost:8080/assets/pnp.js\" you can use: gulp serve --p pnpjs This will allow you to test your changes to the entire bundle live while making updates.","title":"pnpjs"},{"location":"pnpjs/docs/#pnppnpjs","text":"The pnpjs library is a rollup of the core libraries across the @pnp scope and is designed only as a bridge to help folks transition from sp-pnp-js, primarily in scenarios where a single file is being imported via a script tag. It is recommended to not use this rollup library where possible and migrate to the individual libraries .","title":"@pnp/pnpjs"},{"location":"pnpjs/docs/#getting-started","text":"There are two approaches to using this library: the first is to import, the second is to manually extract the bundled file for use in your project.","title":"Getting Started"},{"location":"pnpjs/docs/#install","text":"npm install @pnp/pnpjs --save You can then make use of the pnpjs rollup library within your application. It's structure matches sp-pnp-js, though some things may have changed based on the rolled-up dependencies. import pnp from \"@pnp/pnpjs\" ; pnp . sp . web . get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"Install"},{"location":"pnpjs/docs/#grab-bundle-file","text":"This method is useful if you are primarily working within a script editor web part or similar case where you are not using a build pipeline to bundle your application. Install only this library. npm install @pnp/pnpjs Browse to ./node_modules/@pnp/pnpjs/dist and grab either pnpjs.es5.umd.bundle.js or pnpjs.es5.umd.bundle.min.js depending on your needs. You can then add a script tag referencing this file and you will have a global variable \"pnp\". For example you could paste the following into a script editor web part: < p > Script Editor is on page. < script src = \"https://mysite/site_assets/pnpjs.es5.umd.bundle.min.js\" type = \"text/javascript\" > < script type = \"text/javascript\" > pnp . Logger . subscribe ( new pnp . ConsoleListener ()); pnp . Logger . activeLogLevel = pnp . LogLevel . Info ; pnp . sp . web . get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }); Alternatively to serve the script from the project at \"https://localhost:8080/assets/pnp.js\" you can use: gulp serve --p pnpjs This will allow you to test your changes to the entire bundle live while making updates.","title":"Grab Bundle File"},{"location":"sp/docs/","text":"@pnp/sp \u00b6 This package contains the fluent api used to call the SharePoint rest services. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save Import the library into your application and access the root sp object import { sp } from \"@pnp/sp\" ; ( function main() { // here we will load the current web's title sp . web . select ( \"Title\" ). get (). then ( w => { console . log ( `Web Title: ${ w . Title } ` ); }); })() Getting Started: SharePoint Framework \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save Import the library into your application, update OnInit, and access the root sp object in render import { sp } from \"@pnp/sp\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present sp . setup ({ spfxContext : this.context }); }); } // ... public render () : void { // A simple loading message this . domElement . innerHTML = `Loading...` ; sp . web . select ( \"Title\" ). get (). then ( w => { this . domElement . innerHTML = `Web Title: ${ w . Title } ` ; }); } Getting Started: Nodejs \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { sp } from \"@pnp/sp\" ; import { SPFetchClient } from \"@pnp/nodejs\" ; // do this once per page load sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{your site url}\" , \"{your client id}\" , \"{your client secret}\" ); }, }, }); // now make any calls you need using the configured client sp . web . select ( \"Title\" ). get (). then ( w => { console . log ( `Web Title: ${ w . Title } ` ); }); Library Topics \u00b6 Alias Parameters ALM api Attachments Client-side Pages Features Fields Files List Items Navigation Service Permissions Related Items Search Sharing Site Designs Social SP.Utilities.Utility Tenant Properties Views Webs Comments and Likes UML \u00b6 Graphical UML diagram of @pnp/sp. Right-click the diagram and open in new tab if it is too small.","title":"sp"},{"location":"sp/docs/#pnpsp","text":"This package contains the fluent api used to call the SharePoint rest services.","title":"@pnp/sp"},{"location":"sp/docs/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save Import the library into your application and access the root sp object import { sp } from \"@pnp/sp\" ; ( function main() { // here we will load the current web's title sp . web . select ( \"Title\" ). get (). then ( w => { console . log ( `Web Title: ${ w . Title } ` ); }); })()","title":"Getting Started"},{"location":"sp/docs/#getting-started-sharepoint-framework","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save Import the library into your application, update OnInit, and access the root sp object in render import { sp } from \"@pnp/sp\" ; // ... public onInit () : Promise < void > { return super . onInit (). then ( _ => { // other init code may be present sp . setup ({ spfxContext : this.context }); }); } // ... public render () : void { // A simple loading message this . domElement . innerHTML = `Loading...` ; sp . web . select ( \"Title\" ). get (). then ( w => { this . domElement . innerHTML = `Web Title: ${ w . Title } ` ; }); }","title":"Getting Started: SharePoint Framework"},{"location":"sp/docs/#getting-started-nodejs","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { sp } from \"@pnp/sp\" ; import { SPFetchClient } from \"@pnp/nodejs\" ; // do this once per page load sp . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{your site url}\" , \"{your client id}\" , \"{your client secret}\" ); }, }, }); // now make any calls you need using the configured client sp . web . select ( \"Title\" ). get (). then ( w => { console . log ( `Web Title: ${ w . Title } ` ); });","title":"Getting Started: Nodejs"},{"location":"sp/docs/#library-topics","text":"Alias Parameters ALM api Attachments Client-side Pages Features Fields Files List Items Navigation Service Permissions Related Items Search Sharing Site Designs Social SP.Utilities.Utility Tenant Properties Views Webs Comments and Likes","title":"Library Topics"},{"location":"sp/docs/#uml","text":"Graphical UML diagram of @pnp/sp. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"sp/docs/alias-parameters/","text":"@pnp/sp - Aliased Parameters \u00b6 Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders. To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query. Construct a parameter alias \u00b6 Pattern: !@{label name}::{value} Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\" Example without aliasing \u00b6 import { sp } from \"@pnp/sp\" ; // still works as expected, no aliasing const query = sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/\" ). files . select ( \"Title\" ). top ( 3 ); console . log ( query . toUrl ()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files console . log ( query . toUrlAndQuery ()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 query . get (). then ( r => { console . log ( r ); }); Example with aliasing \u00b6 import { sp } from \"@pnp/sp\" ; // same query with aliasing const query = sp . web . getFolderByServerRelativeUrl ( \"!@p1::/sites/dev/Shared Documents/\" ). files . select ( \"Title\" ). top ( 3 ); console . log ( query . toUrl ()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console . log ( query . toUrlAndQuery ()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query . get (). then ( r => { console . log ( r ); }); Example with aliasing and batching \u00b6 Aliasing is supported with batching as well: import { sp } from \"@pnp/sp\" ; // same query with aliasing and batching const batch = sp . web . createBatch (); const query = sp . web . getFolderByServerRelativeUrl ( \"!@p1::/sites/dev/Shared Documents/\" ). files . select ( \"Title\" ). top ( 3 ); console . log ( query . toUrl ()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console . log ( query . toUrlAndQuery ()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query . inBatch ( batch ). get (). then ( r => { console . log ( r ); }); batch . execute ();","title":"Alias Parameters"},{"location":"sp/docs/alias-parameters/#pnpsp-aliased-parameters","text":"Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders. To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query.","title":"@pnp/sp - Aliased Parameters"},{"location":"sp/docs/alias-parameters/#construct-a-parameter-alias","text":"Pattern: !@{label name}::{value} Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\"","title":"Construct a parameter alias"},{"location":"sp/docs/alias-parameters/#example-without-aliasing","text":"import { sp } from \"@pnp/sp\" ; // still works as expected, no aliasing const query = sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/\" ). files . select ( \"Title\" ). top ( 3 ); console . log ( query . toUrl ()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files console . log ( query . toUrlAndQuery ()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 query . get (). then ( r => { console . log ( r ); });","title":"Example without aliasing"},{"location":"sp/docs/alias-parameters/#example-with-aliasing","text":"import { sp } from \"@pnp/sp\" ; // same query with aliasing const query = sp . web . getFolderByServerRelativeUrl ( \"!@p1::/sites/dev/Shared Documents/\" ). files . select ( \"Title\" ). top ( 3 ); console . log ( query . toUrl ()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console . log ( query . toUrlAndQuery ()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query . get (). then ( r => { console . log ( r ); });","title":"Example with aliasing"},{"location":"sp/docs/alias-parameters/#example-with-aliasing-and-batching","text":"Aliasing is supported with batching as well: import { sp } from \"@pnp/sp\" ; // same query with aliasing and batching const batch = sp . web . createBatch (); const query = sp . web . getFolderByServerRelativeUrl ( \"!@p1::/sites/dev/Shared Documents/\" ). files . select ( \"Title\" ). top ( 3 ); console . log ( query . toUrl ()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console . log ( query . toUrlAndQuery ()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query . inBatch ( batch ). get (). then ( r => { console . log ( r ); }); batch . execute ();","title":"Example with aliasing and batching"},{"location":"sp/docs/alm/","text":"@pnp/sp/appcatalog \u00b6 The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions. Understanding the App Catalog Heirarchy \u00b6 Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation . Reference an App Catalog \u00b6 There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. import { sp } from \"@pnp/sp\" ; // get the curren't context web's app catalog const catalog = sp . web . getAppCatalog (); // you can also chain off the app catalog pnp . sp . web . getAppCatalog (). get (). then ( console . log ); import { sp } from \"@pnp/sp\" ; // you can get the tenant app catalog (or any app catalog) by passing in a url // get the tenant app catalog const tenantCatalog = sp . web . getAppCatalog ( \"https://mytenant.sharepoint.com/sites/appcatalog\" ); // get a different app catalog const catalog = sp . web . getAppCatalog ( \"https://mytenant.sharepoint.com/sites/anothersite\" ); // alternatively you can create a new app catalog instance directly by importing the AppCatalog class import { AppCatalog } from \"@pnp/sp\" ; const catalog = new AppCatalog ( \"https://mytenant.sharepoint.com/sites/dev\" ); // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web import { Web , AppCatalog } from \"@pnp/sp\" ; const web = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const catalog = new AppCatalog ( web ); The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity. List Available Apps \u00b6 The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. // get available apps catalog . get (). then ( console . log ); // get available apps selecting two fields catalog . select ( \"Title\" , \"Deployed\" ). get (). then ( console . log ); Add an App \u00b6 This action must be performed in the context of the tenant app catalog // this represents the file bytes of the app package file const blob = new Blob (); // there is an optional third argument to control overwriting existing files catalog . add ( \"myapp.app\" , blob ). then ( r => { // this is at its core a file add operation so you have access to the response data as well // as a File isntance representing the created file console . log ( JSON . stringify ( r . data , null , 4 )); // all file operations are available r . file . select ( \"Name\" ). get (). then ( console . log ); }); Get an App \u00b6 You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). get (). then ( console . log ); Perform app actions \u00b6 Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success using then and catch. // deploy catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). deploy (). then ( console . log ). catch ( console . error ); // retract catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). retract (). then ( console . log ). catch ( console . error ); // install catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). install (). then ( console . log ). catch ( console . error ); // uninstall catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). uninstall (). then ( console . log ). catch ( console . error ); // upgrade catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). upgrade (). then ( console . log ). catch ( console . error ); // remove catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). remove (). then ( console . log ). catch ( console . error ); Notes \u00b6 The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.","title":"ALM api"},{"location":"sp/docs/alm/#pnpspappcatalog","text":"The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.","title":"@pnp/sp/appcatalog"},{"location":"sp/docs/alm/#understanding-the-app-catalog-heirarchy","text":"Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation .","title":"Understanding the App Catalog Heirarchy"},{"location":"sp/docs/alm/#reference-an-app-catalog","text":"There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. import { sp } from \"@pnp/sp\" ; // get the curren't context web's app catalog const catalog = sp . web . getAppCatalog (); // you can also chain off the app catalog pnp . sp . web . getAppCatalog (). get (). then ( console . log ); import { sp } from \"@pnp/sp\" ; // you can get the tenant app catalog (or any app catalog) by passing in a url // get the tenant app catalog const tenantCatalog = sp . web . getAppCatalog ( \"https://mytenant.sharepoint.com/sites/appcatalog\" ); // get a different app catalog const catalog = sp . web . getAppCatalog ( \"https://mytenant.sharepoint.com/sites/anothersite\" ); // alternatively you can create a new app catalog instance directly by importing the AppCatalog class import { AppCatalog } from \"@pnp/sp\" ; const catalog = new AppCatalog ( \"https://mytenant.sharepoint.com/sites/dev\" ); // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web import { Web , AppCatalog } from \"@pnp/sp\" ; const web = new Web ( \"https://mytenant.sharepoint.com/sites/dev\" ); const catalog = new AppCatalog ( web ); The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.","title":"Reference an App Catalog"},{"location":"sp/docs/alm/#list-available-apps","text":"The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. // get available apps catalog . get (). then ( console . log ); // get available apps selecting two fields catalog . select ( \"Title\" , \"Deployed\" ). get (). then ( console . log );","title":"List Available Apps"},{"location":"sp/docs/alm/#add-an-app","text":"This action must be performed in the context of the tenant app catalog // this represents the file bytes of the app package file const blob = new Blob (); // there is an optional third argument to control overwriting existing files catalog . add ( \"myapp.app\" , blob ). then ( r => { // this is at its core a file add operation so you have access to the response data as well // as a File isntance representing the created file console . log ( JSON . stringify ( r . data , null , 4 )); // all file operations are available r . file . select ( \"Name\" ). get (). then ( console . log ); });","title":"Add an App"},{"location":"sp/docs/alm/#get-an-app","text":"You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). get (). then ( console . log );","title":"Get an App"},{"location":"sp/docs/alm/#perform-app-actions","text":"Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success using then and catch. // deploy catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). deploy (). then ( console . log ). catch ( console . error ); // retract catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). retract (). then ( console . log ). catch ( console . error ); // install catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). install (). then ( console . log ). catch ( console . error ); // uninstall catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). uninstall (). then ( console . log ). catch ( console . error ); // upgrade catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). upgrade (). then ( console . log ). catch ( console . error ); // remove catalog . getAppById ( \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\" ). remove (). then ( console . log ). catch ( console . error );","title":"Perform app actions"},{"location":"sp/docs/alm/#notes","text":"The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.","title":"Notes"},{"location":"sp/docs/attachments/","text":"@pnp/sp/attachments \u00b6 The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below. Get attachments \u00b6 import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); // get all the attachments item . attachmentFiles . get (). then ( v => { console . log ( v ); }); // get a single file by file name item . attachmentFiles . getByName ( \"file.txt\" ). get (). then ( v => { console . log ( v ); }); // select specific properties using odata operators item . attachmentFiles . select ( \"ServerRelativeUrl\" ). get (). then ( v => { console . log ( v ); }); Add an Attachment \u00b6 You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . add ( \"file2.txt\" , \"Here is my content\" ). then ( v => { console . log ( v ); }); Add Multiple \u00b6 This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. const list = sp . web . lists . getByTitle ( \"MyList\" ); var fileInfos : AttachmentFileInfo [] = []; fileInfos . push ({ name : \"My file name 1\" , content : \"string, blob, or array\" }); fileInfos . push ({ name : \"My file name 2\" , content : \"string, blob, or array\" }); list . items . getById ( 2 ). attachmentFiles . addMultiple ( fileInfos ). then ( r => { console . log ( r ); }); Delete Multiple \u00b6 const list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 2 ). attachmentFiles . deleteMultiple ( \"1.txt\" , \"2.txt\" ). then ( r => { console . log ( r ); }); Read Attachment Content \u00b6 You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file.txt\" ). getText (). then ( v => { console . log ( v ); }); // use this in the browser, does not work in nodejs item . attachmentFiles . getByName ( \"file.mp4\" ). getBlob (). then ( v => { console . log ( v ); }); // use this in nodejs item . attachmentFiles . getByName ( \"file.mp4\" ). getBuffer (). then ( v => { console . log ( v ); }); // file must be valid json item . attachmentFiles . getByName ( \"file.json\" ). getJSON (). then ( v => { console . log ( v ); }); Update Attachment Content \u00b6 You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file2.txt\" ). setContent ( \"My new content!!!\" ). then ( v => { console . log ( v ); }); Delete Attachment \u00b6 import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file2.txt\" ). delete (). then ( v => { console . log ( v ); }); Recycle Attachment \u00b6 Added in 1.2.4 Delete the attachment and send it to recycle bin import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file2.txt\" ). recycle (). then ( v => { console . log ( v ); }); Recycle Multiple Attachments \u00b6 Added in 1.2.4 Delete multiple attachments and send them to recycle bin import { sp } from \"@pnp/sp\" ; const list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 2 ). attachmentFiles . recycleMultiple ( \"1.txt\" , \"2.txt\" ). then ( r => { console . log ( r ); });","title":"Attachments"},{"location":"sp/docs/attachments/#pnpspattachments","text":"The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.","title":"@pnp/sp/attachments"},{"location":"sp/docs/attachments/#get-attachments","text":"import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); // get all the attachments item . attachmentFiles . get (). then ( v => { console . log ( v ); }); // get a single file by file name item . attachmentFiles . getByName ( \"file.txt\" ). get (). then ( v => { console . log ( v ); }); // select specific properties using odata operators item . attachmentFiles . select ( \"ServerRelativeUrl\" ). get (). then ( v => { console . log ( v ); });","title":"Get attachments"},{"location":"sp/docs/attachments/#add-an-attachment","text":"You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . add ( \"file2.txt\" , \"Here is my content\" ). then ( v => { console . log ( v ); });","title":"Add an Attachment"},{"location":"sp/docs/attachments/#add-multiple","text":"This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. const list = sp . web . lists . getByTitle ( \"MyList\" ); var fileInfos : AttachmentFileInfo [] = []; fileInfos . push ({ name : \"My file name 1\" , content : \"string, blob, or array\" }); fileInfos . push ({ name : \"My file name 2\" , content : \"string, blob, or array\" }); list . items . getById ( 2 ). attachmentFiles . addMultiple ( fileInfos ). then ( r => { console . log ( r ); });","title":"Add Multiple"},{"location":"sp/docs/attachments/#delete-multiple","text":"const list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 2 ). attachmentFiles . deleteMultiple ( \"1.txt\" , \"2.txt\" ). then ( r => { console . log ( r ); });","title":"Delete Multiple"},{"location":"sp/docs/attachments/#read-attachment-content","text":"You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file.txt\" ). getText (). then ( v => { console . log ( v ); }); // use this in the browser, does not work in nodejs item . attachmentFiles . getByName ( \"file.mp4\" ). getBlob (). then ( v => { console . log ( v ); }); // use this in nodejs item . attachmentFiles . getByName ( \"file.mp4\" ). getBuffer (). then ( v => { console . log ( v ); }); // file must be valid json item . attachmentFiles . getByName ( \"file.json\" ). getJSON (). then ( v => { console . log ( v ); });","title":"Read Attachment Content"},{"location":"sp/docs/attachments/#update-attachment-content","text":"You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file2.txt\" ). setContent ( \"My new content!!!\" ). then ( v => { console . log ( v ); });","title":"Update Attachment Content"},{"location":"sp/docs/attachments/#delete-attachment","text":"import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file2.txt\" ). delete (). then ( v => { console . log ( v ); });","title":"Delete Attachment"},{"location":"sp/docs/attachments/#recycle-attachment","text":"Added in 1.2.4 Delete the attachment and send it to recycle bin import { sp } from \"@pnp/sp\" ; let item = sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( 1 ); item . attachmentFiles . getByName ( \"file2.txt\" ). recycle (). then ( v => { console . log ( v ); });","title":"Recycle Attachment"},{"location":"sp/docs/attachments/#recycle-multiple-attachments","text":"Added in 1.2.4 Delete multiple attachments and send them to recycle bin import { sp } from \"@pnp/sp\" ; const list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 2 ). attachmentFiles . recycleMultiple ( \"1.txt\" , \"2.txt\" ). then ( r => { console . log ( r ); });","title":"Recycle Multiple Attachments"},{"location":"sp/docs/client-side-pages/","text":"@pnp/sp/clientsidepages \u00b6 The ability to manage client-side pages is a capability introduced in version 1.0.2 of @pnp/sp. Through the methods described you can add and edit \"modern\" pages in SharePoint sites. Add Client-side page \u00b6 Using the addClientSidePage you can add a new client side page to a site, specifying the filename. import { sp } from \"@pnp/sp\" ; const page = await sp . web . addClientSidePage ( `file-name` ); // OR const page = await sp . web . addClientSidePage ( `file-name` , `Page Display Title` ); Added in 1.0.5 you can also add a client side page using the list path. This gets around potential language issues with list title. You must specify the list path when calling this method in addition to the new page's filename. import { sp } from \"@pnp/sp\" ; const page = await sp . web . addClientSidePageByPath ( `file-name` , \"/sites/dev/SitePages\" ); Load Client-side page \u00b6 You can also load an existing page based on the file representing that page. Note that the static fromFile returns a promise which resolves so the loaded page. Here we are showing use of the getFileByServerRelativeUrl method to get the File instance, but any of the ways of getting a File instance will work. Also note we are passing the File instance, not the file content. import { sp , ClientSidePage , } from \"@pnp/sp\" ; const page = await ClientSidePage . fromFile ( sp . web . getFileByServerRelativeUrl ( \"/sites/dev/SitePages/ExistingFile.aspx\" )); The remaining examples below reference a variable \"page\" which is assumed to be a ClientSidePage instance loaded through one of the above means. Add Controls \u00b6 A client-side page is made up of sections, which have columns, which contain controls. A new page will have none of these and an existing page may have any combination of these. There are a few rules to understand how sections and columns layout on a page for display. A section is a horizontal piece of a page that extends 100% of the page width. A page with multiple sections will stack these sections based on the section's order property - a 1 based index. Within a section you can have one or more columns. Each column is ordered left to right based on the column's order property. The width of each column is controlled by the factor property whose value is one of 0, 2, 4, 6, 8, 10, or 12. The columns in a section should have factors that add up to 12. Meaning if you wanted to have two equal columns you can set a factor of 6 for each. A page can have empty columns. import { sp , ClientSideText , } from \"@pnp/sp\" ; // this code adds a section, and then adds a control to that section. The control is added to the section's defaultColumn, and if there are no columns a single // column of factor 12 is created as a default. Here we add the ClientSideText part page . addSection (). addControl ( new ClientSideText ( \"@pnp/sp is a great library!\" )); // here we add a section, add two columns, and add a text control to the second section so it will appear on the right of the page // add and get a reference to a new section const section = page . addSection (); // add a column of factor 6 section . addColumn ( 6 ); // add and get a reference to a new column of factor 6 const column = section . addColumn ( 6 ); // add a text control to the second new column column . addControl ( new ClientSideText ( \"Be sure to check out the @pnp docs at https://pnp.github.io/pnpjs/\" )); // we need to save our content changes await page . save (); Add Client-side Web Parts \u00b6 Beyond the text control above you can also add any of the available client-side web parts in a given site. To find out what web parts are available you first call the web's getClientSideWebParts method. Once you have a list of parts you need to find the defintion you want to use, here we get the Embed web part whose's id is \"490d7c76-1824-45b2-9de3-676421c997fa\" (at least in one farm, your mmv). import { sp , ClientSideWebpart , ClientSideWebpartPropertyTypes , } from \"@pnp/sp\" ; // this will be a ClientSidePageComponent array // this can be cached on the client in production scenarios const partDefs = await sp . web . getClientSideWebParts (); // find the definition we want, here by id const partDef = partDefs . filter ( c => c . Id === \"490d7c76-1824-45b2-9de3-676421c997fa\" ); // optionally ensure you found the def if ( partDef . length < 1 ) { // we didn't find it so we throw an error throw new Error ( \"Could not find the web part\" ); } // create a ClientWebPart instance from the definition const part = ClientSideWebpart . fromComponentDef ( partDef [ 0 ]); // set the properties on the web part. Here we have imported the ClientSideWebpartPropertyTypes module and can use that to type // the available settings object. You can use your own types or help us out and add some typings to the module :). // here for the embed web part we only have to supply an embedCode - in this case a youtube video. part . setProperties < ClientSideWebpartPropertyTypes . Embed > ({ embedCode : \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\" , }); // we add that part to a new section page . addSection (). addControl ( part ); // save our content changes back to the server await page . save (); Find Controls \u00b6 Added in 1.0.3 You can use the either of the two available method to locate controls within a page. These method search through all sections, columns, and controls returning the first instance that meets the supplied criteria. import { ClientSideWebPart } from \"@pnp/sp\" ; // find a control by instance id const control1 = page . findControlById ( \"b99bfccc-164e-4d3d-9b96-da48db62eb78\" ); // type the returned control const control2 = page . findControlById < ClientSideWebPart > ( \"c99bfccc-164e-4d3d-9b96-da48db62eb78\" ); const control3 = page . findControlById < ClientSideText > ( \"a99bfccc-164e-4d3d-9b96-da48db62eb78\" ); // use any predicate to find a control const control4 = page2 . findControl < ClientSideWebpart > (( c : CanvasControl ) => { // any logic you wish can be used on the control here // return true to return that control return c . order > 3 ; }); Control Comments \u00b6 You can choose to enable or disable comments on a page using these methods // indicates if comments are disabled, not valid until the page is loaded (Added in _1.0.3_) page . commentsDisabled // enable comments await page . enableComments (); // disable comments await page . disableComments (); Like/Unlike Client-side page, get like information about page \u00b6 Added in 1.2.4 You can like or unlike a modern page. You can also get information about the likes (i.e like Count and which users liked the page) // Like a Client-side page (Added in _1.2.4_) await page . like (); // Unlike a Client-side page await page . unlike (); // Get liked by information such as like count and user's who liked the page await page . getLikedByInformation (); Sample \u00b6 The below sample shows the process to add a Yammer feed webpart to the page. The properties required as well as the data version are found by adding the part using the UI and reviewing the values. Some or all of these may be discoverable using Yammer APIs . An identical process can be used to add web parts of any type by adjusting the definition, data version, and properties appropriately. // get webpart defs const defs = await sp . web . getClientSideWebParts (); // this is the id of the definition in my farm const yammerPartDef = defs . filter ( d => d . Id === \"31e9537e-f9dc-40a4-8834-0e3b7df418bc\" )[ 0 ]; // page file const file = sp . web . getFileByServerRelativePath ( \"/sites/dev/SitePages/Testing_kVKF.aspx\" ); // create page instance const page = await ClientSidePage . fromFile ( file ); // create part instance from definition const part = ClientSideWebpart . fromComponentDef ( yammerPartDef ); // update data version part . dataVersion = \"1.5\" ; // set the properties required part . setProperties ({ feedType : 0 , isSuiteConnected : false , mode : 2 , networkId : 9999999 , yammerEmbedContainerHeight : 400 , yammerFeedURL : \"\" , yammerGroupId : - 1 , yammerGroupMugshotUrl : \"https://mug0.assets-yammer.com/mugshot/images/{width}x{height}/all_company.png\" , yammerGroupName : \"All Company\" , yammerGroupUrl : \"https://www.yammer.com/{tenant}/#/threads/company?type=general\" , }); // add to the section/column you want page . sections [ 0 ]. addControl ( part ); // persist changes page . save ();","title":"Client-side Pages"},{"location":"sp/docs/client-side-pages/#pnpspclientsidepages","text":"The ability to manage client-side pages is a capability introduced in version 1.0.2 of @pnp/sp. Through the methods described you can add and edit \"modern\" pages in SharePoint sites.","title":"@pnp/sp/clientsidepages"},{"location":"sp/docs/client-side-pages/#add-client-side-page","text":"Using the addClientSidePage you can add a new client side page to a site, specifying the filename. import { sp } from \"@pnp/sp\" ; const page = await sp . web . addClientSidePage ( `file-name` ); // OR const page = await sp . web . addClientSidePage ( `file-name` , `Page Display Title` ); Added in 1.0.5 you can also add a client side page using the list path. This gets around potential language issues with list title. You must specify the list path when calling this method in addition to the new page's filename. import { sp } from \"@pnp/sp\" ; const page = await sp . web . addClientSidePageByPath ( `file-name` , \"/sites/dev/SitePages\" );","title":"Add Client-side page"},{"location":"sp/docs/client-side-pages/#load-client-side-page","text":"You can also load an existing page based on the file representing that page. Note that the static fromFile returns a promise which resolves so the loaded page. Here we are showing use of the getFileByServerRelativeUrl method to get the File instance, but any of the ways of getting a File instance will work. Also note we are passing the File instance, not the file content. import { sp , ClientSidePage , } from \"@pnp/sp\" ; const page = await ClientSidePage . fromFile ( sp . web . getFileByServerRelativeUrl ( \"/sites/dev/SitePages/ExistingFile.aspx\" )); The remaining examples below reference a variable \"page\" which is assumed to be a ClientSidePage instance loaded through one of the above means.","title":"Load Client-side page"},{"location":"sp/docs/client-side-pages/#add-controls","text":"A client-side page is made up of sections, which have columns, which contain controls. A new page will have none of these and an existing page may have any combination of these. There are a few rules to understand how sections and columns layout on a page for display. A section is a horizontal piece of a page that extends 100% of the page width. A page with multiple sections will stack these sections based on the section's order property - a 1 based index. Within a section you can have one or more columns. Each column is ordered left to right based on the column's order property. The width of each column is controlled by the factor property whose value is one of 0, 2, 4, 6, 8, 10, or 12. The columns in a section should have factors that add up to 12. Meaning if you wanted to have two equal columns you can set a factor of 6 for each. A page can have empty columns. import { sp , ClientSideText , } from \"@pnp/sp\" ; // this code adds a section, and then adds a control to that section. The control is added to the section's defaultColumn, and if there are no columns a single // column of factor 12 is created as a default. Here we add the ClientSideText part page . addSection (). addControl ( new ClientSideText ( \"@pnp/sp is a great library!\" )); // here we add a section, add two columns, and add a text control to the second section so it will appear on the right of the page // add and get a reference to a new section const section = page . addSection (); // add a column of factor 6 section . addColumn ( 6 ); // add and get a reference to a new column of factor 6 const column = section . addColumn ( 6 ); // add a text control to the second new column column . addControl ( new ClientSideText ( \"Be sure to check out the @pnp docs at https://pnp.github.io/pnpjs/\" )); // we need to save our content changes await page . save ();","title":"Add Controls"},{"location":"sp/docs/client-side-pages/#add-client-side-web-parts","text":"Beyond the text control above you can also add any of the available client-side web parts in a given site. To find out what web parts are available you first call the web's getClientSideWebParts method. Once you have a list of parts you need to find the defintion you want to use, here we get the Embed web part whose's id is \"490d7c76-1824-45b2-9de3-676421c997fa\" (at least in one farm, your mmv). import { sp , ClientSideWebpart , ClientSideWebpartPropertyTypes , } from \"@pnp/sp\" ; // this will be a ClientSidePageComponent array // this can be cached on the client in production scenarios const partDefs = await sp . web . getClientSideWebParts (); // find the definition we want, here by id const partDef = partDefs . filter ( c => c . Id === \"490d7c76-1824-45b2-9de3-676421c997fa\" ); // optionally ensure you found the def if ( partDef . length < 1 ) { // we didn't find it so we throw an error throw new Error ( \"Could not find the web part\" ); } // create a ClientWebPart instance from the definition const part = ClientSideWebpart . fromComponentDef ( partDef [ 0 ]); // set the properties on the web part. Here we have imported the ClientSideWebpartPropertyTypes module and can use that to type // the available settings object. You can use your own types or help us out and add some typings to the module :). // here for the embed web part we only have to supply an embedCode - in this case a youtube video. part . setProperties < ClientSideWebpartPropertyTypes . Embed > ({ embedCode : \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\" , }); // we add that part to a new section page . addSection (). addControl ( part ); // save our content changes back to the server await page . save ();","title":"Add Client-side Web Parts"},{"location":"sp/docs/client-side-pages/#find-controls","text":"Added in 1.0.3 You can use the either of the two available method to locate controls within a page. These method search through all sections, columns, and controls returning the first instance that meets the supplied criteria. import { ClientSideWebPart } from \"@pnp/sp\" ; // find a control by instance id const control1 = page . findControlById ( \"b99bfccc-164e-4d3d-9b96-da48db62eb78\" ); // type the returned control const control2 = page . findControlById < ClientSideWebPart > ( \"c99bfccc-164e-4d3d-9b96-da48db62eb78\" ); const control3 = page . findControlById < ClientSideText > ( \"a99bfccc-164e-4d3d-9b96-da48db62eb78\" ); // use any predicate to find a control const control4 = page2 . findControl < ClientSideWebpart > (( c : CanvasControl ) => { // any logic you wish can be used on the control here // return true to return that control return c . order > 3 ; });","title":"Find Controls"},{"location":"sp/docs/client-side-pages/#control-comments","text":"You can choose to enable or disable comments on a page using these methods // indicates if comments are disabled, not valid until the page is loaded (Added in _1.0.3_) page . commentsDisabled // enable comments await page . enableComments (); // disable comments await page . disableComments ();","title":"Control Comments"},{"location":"sp/docs/client-side-pages/#likeunlike-client-side-page-get-like-information-about-page","text":"Added in 1.2.4 You can like or unlike a modern page. You can also get information about the likes (i.e like Count and which users liked the page) // Like a Client-side page (Added in _1.2.4_) await page . like (); // Unlike a Client-side page await page . unlike (); // Get liked by information such as like count and user's who liked the page await page . getLikedByInformation ();","title":"Like/Unlike Client-side page, get like information about page"},{"location":"sp/docs/client-side-pages/#sample","text":"The below sample shows the process to add a Yammer feed webpart to the page. The properties required as well as the data version are found by adding the part using the UI and reviewing the values. Some or all of these may be discoverable using Yammer APIs . An identical process can be used to add web parts of any type by adjusting the definition, data version, and properties appropriately. // get webpart defs const defs = await sp . web . getClientSideWebParts (); // this is the id of the definition in my farm const yammerPartDef = defs . filter ( d => d . Id === \"31e9537e-f9dc-40a4-8834-0e3b7df418bc\" )[ 0 ]; // page file const file = sp . web . getFileByServerRelativePath ( \"/sites/dev/SitePages/Testing_kVKF.aspx\" ); // create page instance const page = await ClientSidePage . fromFile ( file ); // create part instance from definition const part = ClientSideWebpart . fromComponentDef ( yammerPartDef ); // update data version part . dataVersion = \"1.5\" ; // set the properties required part . setProperties ({ feedType : 0 , isSuiteConnected : false , mode : 2 , networkId : 9999999 , yammerEmbedContainerHeight : 400 , yammerFeedURL : \"\" , yammerGroupId : - 1 , yammerGroupMugshotUrl : \"https://mug0.assets-yammer.com/mugshot/images/{width}x{height}/all_company.png\" , yammerGroupName : \"All Company\" , yammerGroupUrl : \"https://www.yammer.com/{tenant}/#/threads/company?type=general\" , }); // add to the section/column you want page . sections [ 0 ]. addControl ( part ); // persist changes page . save ();","title":"Sample"},{"location":"sp/docs/comments-likes/","text":"@pnp/sp/comments and likes \u00b6 Likes and comments in the context of modern sites are based on list items, meaning the operations branch from the Item class. To load an item you can refer to the guidance in the items article . If you want to set the likes or comments on a modern page and don't know the item id but do know the url you can first load the file and then use the getItem method to get an item instance: These APIs are currently in BETA and are subject to change or may not work on all tenants. import { sp } from \"@pnp/sp\" ; const item = await sp . web . getFileByServerRelativeUrl ( \"/sites/dev/SitePages/Test_8q5L.aspx\" ). getItem (); // as an example, or any of the below options await item . like (); The below examples use a variable named \"item\" which is taken to represent an instance of the Item class. Comments \u00b6 Get Comments \u00b6 const comments = await item . comments . get (); You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); // these will be Comment instances in the array comments [ 0 ]. replies . add ({ text : \"#PnPjs is pretty ok!\" }); //load the top 20 replies and comments for an item including likedBy information const comments = await item . comments . expand ( \"replies\" , \"likedBy\" , \"replies/likedBy\" ). top ( 20 ). get (); Add Comment \u00b6 // you can add a comment as a string item . comments . add ( \"string comment\" ); // or you can add it as an object to include mentions item . comments . add ({ text : \"comment from object property\" }); Delete a Comment \u00b6 import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); // these will be Comment instances in the array comments [ 0 ]. delete () Like Comment \u00b6 import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); // these will be Comment instances in the array comments [ 0 ]. like () Unlike Comment \u00b6 import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); comments [ 0 ]. unlike () Reply to a Comment \u00b6 import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); const comment : Comment & CommentData = await comments [ 0 ]. replies . add ({ text : \"#PnPjs is pretty ok!\" }); Load Replies to a Comment \u00b6 import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); const replies = await comments [ 0 ]. replies . get (); Like \u00b6 You can like items and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. import { LikeData } from \"@pnp/sp\" ; // like an item await item . like (); // unlike an item await item . unlike (); // get the liked by information const likedByData : LikeData [] = await item . getLikedBy ();","title":"Comments and Likes"},{"location":"sp/docs/comments-likes/#pnpspcomments-and-likes","text":"Likes and comments in the context of modern sites are based on list items, meaning the operations branch from the Item class. To load an item you can refer to the guidance in the items article . If you want to set the likes or comments on a modern page and don't know the item id but do know the url you can first load the file and then use the getItem method to get an item instance: These APIs are currently in BETA and are subject to change or may not work on all tenants. import { sp } from \"@pnp/sp\" ; const item = await sp . web . getFileByServerRelativeUrl ( \"/sites/dev/SitePages/Test_8q5L.aspx\" ). getItem (); // as an example, or any of the below options await item . like (); The below examples use a variable named \"item\" which is taken to represent an instance of the Item class.","title":"@pnp/sp/comments and likes"},{"location":"sp/docs/comments-likes/#comments","text":"","title":"Comments"},{"location":"sp/docs/comments-likes/#get-comments","text":"const comments = await item . comments . get (); You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); // these will be Comment instances in the array comments [ 0 ]. replies . add ({ text : \"#PnPjs is pretty ok!\" }); //load the top 20 replies and comments for an item including likedBy information const comments = await item . comments . expand ( \"replies\" , \"likedBy\" , \"replies/likedBy\" ). top ( 20 ). get ();","title":"Get Comments"},{"location":"sp/docs/comments-likes/#add-comment","text":"// you can add a comment as a string item . comments . add ( \"string comment\" ); // or you can add it as an object to include mentions item . comments . add ({ text : \"comment from object property\" });","title":"Add Comment"},{"location":"sp/docs/comments-likes/#delete-a-comment","text":"import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); // these will be Comment instances in the array comments [ 0 ]. delete ()","title":"Delete a Comment"},{"location":"sp/docs/comments-likes/#like-comment","text":"import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); // these will be Comment instances in the array comments [ 0 ]. like ()","title":"Like Comment"},{"location":"sp/docs/comments-likes/#unlike-comment","text":"import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); comments [ 0 ]. unlike ()","title":"Unlike Comment"},{"location":"sp/docs/comments-likes/#reply-to-a-comment","text":"import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); const comment : Comment & CommentData = await comments [ 0 ]. replies . add ({ text : \"#PnPjs is pretty ok!\" });","title":"Reply to a Comment"},{"location":"sp/docs/comments-likes/#load-replies-to-a-comment","text":"import { spODataEntityArray , Comment , CommentData } from \"@pnp/sp\" ; const comments = await item . comments . get ( spODataEntityArray < Comment , CommentData > ( Comment )); const replies = await comments [ 0 ]. replies . get ();","title":"Load Replies to a Comment"},{"location":"sp/docs/comments-likes/#like","text":"You can like items and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. import { LikeData } from \"@pnp/sp\" ; // like an item await item . like (); // unlike an item await item . unlike (); // get the liked by information const likedByData : LikeData [] = await item . getLikedBy ();","title":"Like"},{"location":"sp/docs/content-types/","text":"@pnp/sp/content types \u00b6 Set Folder Unique Content Type Order \u00b6 interface OrderData { ContentTypeOrder : { StringValue : string }[]; UniqueContentTypeOrder ?: { StringValue : string }[]; } const folder = sp . web . lists . getById ( \"{list id guid}\" ). rootFolder ; // here you need to see if there are unique content type orders already or just the default const existingOrders = await folder . select ( \"ContentTypeOrder\" , \"UniqueContentTypeOrder\" ). get < OrderData > (); const activeOrder = existingOrders . UniqueContentTypeOrder ? existingOrders.UniqueContentTypeOrder : existingOrders.ContentTypeOrder ; // manipulate the order here however you want (I am just reversing the array as an example) const newOrder = activeOrder . reverse (); // update the content type order thusly: await folder . update ({ UniqueContentTypeOrder : { __metadata : { type : \"Collection(SP.ContentTypeId)\" }, results : newOrder , }, });","title":"Content Types"},{"location":"sp/docs/content-types/#pnpspcontent-types","text":"","title":"@pnp/sp/content types"},{"location":"sp/docs/content-types/#set-folder-unique-content-type-order","text":"interface OrderData { ContentTypeOrder : { StringValue : string }[]; UniqueContentTypeOrder ?: { StringValue : string }[]; } const folder = sp . web . lists . getById ( \"{list id guid}\" ). rootFolder ; // here you need to see if there are unique content type orders already or just the default const existingOrders = await folder . select ( \"ContentTypeOrder\" , \"UniqueContentTypeOrder\" ). get < OrderData > (); const activeOrder = existingOrders . UniqueContentTypeOrder ? existingOrders.UniqueContentTypeOrder : existingOrders.ContentTypeOrder ; // manipulate the order here however you want (I am just reversing the array as an example) const newOrder = activeOrder . reverse (); // update the content type order thusly: await folder . update ({ UniqueContentTypeOrder : { __metadata : { type : \"Collection(SP.ContentTypeId)\" }, results : newOrder , }, });","title":"Set Folder Unique Content Type Order"},{"location":"sp/docs/entity-merging/","text":"@pnp/sp - entity merging \u00b6 Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its represending type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples. Request a single entity \u00b6 If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query. import { sp , spODataEntity , Item } from \"@pnp/sp\" ; // interface defining the returned properites interface MyProps { Id : number ; } try { // get a list item laoded with data and merged into an instance of Item const item = await sp . web . lists . getByTitle ( \"ListTitle\" ). items . getById ( 1 ). get ( spODataEntity < Item , MyProps > ( Item )); // log the item id, all properties specified in MyProps will be type checked Logger . write ( `Item id: ${ item . Id } ` ); // now we can call update because we have an instance of the Item type to work with as well await item . update ({ Title : \"New title.\" , }); } catch ( e ) { Logger . error ( e ); } Request a collection \u00b6 The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method. import { sp , spODataEntityArray , Item } from \"@pnp/sp\" ; // interface defining the returned properites interface MyProps { Id : number ; Title : string ; } try { // get a list item laoded with data and merged into an instance of Item const items = await sp . web . lists . getByTitle ( \"ListTitle\" ). items . select ( \"Id\" , \"Title\" ). get ( spODataEntityArray < Item , MyProps > ( Item )); Logger . write ( `Item id: ${ items . length } ` ); Logger . write ( `Item id: ${ items [ 0 ]. Title } ` ); // now we can call update because we have an instance of the Item type to work with as well await items [ 0 ]. update ({ Title : \"New title.\" , }); } catch ( e ) { Logger . error ( e ); } Use with Item getPaged \u00b6 Added in 1.3.4 Starting with 1.3.4 you can now include entity merging in the getPaged command as shown below. This approach will work with any objects matching the required factory pattern. // create Item instances with the defined property Title const items = await sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). getPaged ( spODataEntityArray < Item , { Title : string } > ( Item )); console . log ( items . results . length ); // now invoke methods on the Item object const perms = await items . results [ 0 ]. getCurrentUserEffectivePermissions (); console . log ( JSON . stringify ( perms , null , 2 )); // you can also type the result slightly differently if you prefer this, but the results are the same functionally. const items2 = await sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). getPaged < ( Item & { Title : string })[] > ( spODataEntityArray ( Item ));","title":"Entity Merging"},{"location":"sp/docs/entity-merging/#pnpsp-entity-merging","text":"Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its represending type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples.","title":"@pnp/sp - entity merging"},{"location":"sp/docs/entity-merging/#request-a-single-entity","text":"If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query. import { sp , spODataEntity , Item } from \"@pnp/sp\" ; // interface defining the returned properites interface MyProps { Id : number ; } try { // get a list item laoded with data and merged into an instance of Item const item = await sp . web . lists . getByTitle ( \"ListTitle\" ). items . getById ( 1 ). get ( spODataEntity < Item , MyProps > ( Item )); // log the item id, all properties specified in MyProps will be type checked Logger . write ( `Item id: ${ item . Id } ` ); // now we can call update because we have an instance of the Item type to work with as well await item . update ({ Title : \"New title.\" , }); } catch ( e ) { Logger . error ( e ); }","title":"Request a single entity"},{"location":"sp/docs/entity-merging/#request-a-collection","text":"The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method. import { sp , spODataEntityArray , Item } from \"@pnp/sp\" ; // interface defining the returned properites interface MyProps { Id : number ; Title : string ; } try { // get a list item laoded with data and merged into an instance of Item const items = await sp . web . lists . getByTitle ( \"ListTitle\" ). items . select ( \"Id\" , \"Title\" ). get ( spODataEntityArray < Item , MyProps > ( Item )); Logger . write ( `Item id: ${ items . length } ` ); Logger . write ( `Item id: ${ items [ 0 ]. Title } ` ); // now we can call update because we have an instance of the Item type to work with as well await items [ 0 ]. update ({ Title : \"New title.\" , }); } catch ( e ) { Logger . error ( e ); }","title":"Request a collection"},{"location":"sp/docs/entity-merging/#use-with-item-getpaged","text":"Added in 1.3.4 Starting with 1.3.4 you can now include entity merging in the getPaged command as shown below. This approach will work with any objects matching the required factory pattern. // create Item instances with the defined property Title const items = await sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). getPaged ( spODataEntityArray < Item , { Title : string } > ( Item )); console . log ( items . results . length ); // now invoke methods on the Item object const perms = await items . results [ 0 ]. getCurrentUserEffectivePermissions (); console . log ( JSON . stringify ( perms , null , 2 )); // you can also type the result slightly differently if you prefer this, but the results are the same functionally. const items2 = await sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). getPaged < ( Item & { Title : string })[] > ( spODataEntityArray ( Item ));","title":"Use with Item getPaged"},{"location":"sp/docs/features/","text":"@pnp/sp/features \u00b6 Features are used by SharePoint to package a set of functionality and either enable (activate) or disable (deactivate) that functionality based on requirements for a specific site. You can manage feature activation using the library as shown below. Note that the features collection only contains active features. List all Features \u00b6 import { sp } from \"@pnp/sp\" ; let web = sp . web ; // get all the active features web . features . get (). then ( f => { console . log ( f ); }); // select properties using odata operators web . features . select ( \"DisplayName\" , \"DefinitionId\" ). get (). then ( f => { console . log ( f ); }); // get a particular feature by id web . features . getById ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). select ( \"DisplayName\" , \"DefinitionId\" ). get (). then ( f => { console . log ( f ); }); // get features using odata operators web . features . filter ( \"DisplayName eq 'MDSFeature'\" ). get (). then ( f => { console . log ( f ); }); Activate a Feature \u00b6 To activate a feature you must know the feature id. You can optionally force activation - if you aren't sure don't use force. import { sp } from \"@pnp/sp\" ; let web = sp . web ; // activate the minimum download strategy feature web . features . add ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). then ( f => { console . log ( f ); }); Deactivate a Feature \u00b6 import { sp } from \"@pnp/sp\" ; let web = sp . web ; web . features . remove ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). then ( f => { console . log ( f ); }); // you can also deactivate a feature but going through the collection's remove method is faster web . features . getById ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). deactivate (). then ( f => { console . log ( f ); });","title":"Features"},{"location":"sp/docs/features/#pnpspfeatures","text":"Features are used by SharePoint to package a set of functionality and either enable (activate) or disable (deactivate) that functionality based on requirements for a specific site. You can manage feature activation using the library as shown below. Note that the features collection only contains active features.","title":"@pnp/sp/features"},{"location":"sp/docs/features/#list-all-features","text":"import { sp } from \"@pnp/sp\" ; let web = sp . web ; // get all the active features web . features . get (). then ( f => { console . log ( f ); }); // select properties using odata operators web . features . select ( \"DisplayName\" , \"DefinitionId\" ). get (). then ( f => { console . log ( f ); }); // get a particular feature by id web . features . getById ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). select ( \"DisplayName\" , \"DefinitionId\" ). get (). then ( f => { console . log ( f ); }); // get features using odata operators web . features . filter ( \"DisplayName eq 'MDSFeature'\" ). get (). then ( f => { console . log ( f ); });","title":"List all Features"},{"location":"sp/docs/features/#activate-a-feature","text":"To activate a feature you must know the feature id. You can optionally force activation - if you aren't sure don't use force. import { sp } from \"@pnp/sp\" ; let web = sp . web ; // activate the minimum download strategy feature web . features . add ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). then ( f => { console . log ( f ); });","title":"Activate a Feature"},{"location":"sp/docs/features/#deactivate-a-feature","text":"import { sp } from \"@pnp/sp\" ; let web = sp . web ; web . features . remove ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). then ( f => { console . log ( f ); }); // you can also deactivate a feature but going through the collection's remove method is faster web . features . getById ( \"87294c72-f260-42f3-a41b-981a2ffce37a\" ). deactivate (). then ( f => { console . log ( f ); });","title":"Deactivate a Feature"},{"location":"sp/docs/fields/","text":"@pnp/sp/fields \u00b6 Fields allow you to store typed information within a SharePoint list. There are many types of fields and the library seeks to simplify working with the most common types. Fields exist in both site collections (site columns) or lists (list columns) and you can add/modify/delete them at either of these levels. Get Fields \u00b6 import { sp } from \"@pnp/sp\" ; let web = sp . web ; // get all the fields in a web web . fields . get (). then ( f => { console . log ( f ); }); // you can use odata operators on the fields collection web . fields . select ( \"Title\" , \"InternalName\" , \"TypeAsString\" ). top ( 10 ). orderBy ( \"Id\" ). get (). then ( f => { console . log ( f ); }); // get all the available fields in a web (includes parent web's fields) web . availablefields . get (). then ( f => { console . log ( f ); }); // get the fields in a list web . lists . getByTitle ( \"MyList\" ). fields . get (). then ( f => { console . log ( f ); }); // you can also get individual fields using getById, getByTitle, or getByInternalNameOrTitle web . fields . getById ( \"dee9c205-2537-44d6-94e2-7c957e6ebe6e\" ). get (). then ( f => { console . log ( f ); }); web . fields . getByTitle ( \"MyField4\" ). get (). then ( f => { console . log ( f ); }); web . fields . getByInternalNameOrTitle ( \"MyField4\" ). get (). then ( f => { console . log ( f ); }); Filtering Fields \u00b6 Sometimes you only want a subset of fields from the collection. Below are some examples of using the filter operator with the fields collection. import { sp } from '@pnp/sp' ; const list = sp . web . lists . getByTitle ( 'Custom' ); // Fields which can be updated const filter1 = `Hidden eq false and ReadOnlyField eq false` ; list . fields . select ( 'InternalName' ). filter ( filter1 ). get (). then ( fields => { console . log ( `Can be updated: ${ fields . map ( f => f . InternalName ). join ( ', ' ) } ` ); // Title, ...Custom, ContentType, Attachments }); // Only custom field const filter2 = `Hidden eq false and CanBeDeleted eq true` ; list . fields . select ( 'InternalName' ). filter ( filter2 ). get (). then ( fields => { console . log ( `Custom fields: ${ fields . map ( f => f . InternalName ). join ( ', ' ) } ` ); // ...Custom }); // Application specific fields const includeFields = [ 'Title' , 'Author' , 'Editor' , 'Modified' , 'Created' ]; const filter3 = `Hidden eq false and (ReadOnlyField eq false or ( ${ includeFields . map ( field => `InternalName eq ' ${ field } '` ). join ( ' or ' ) } ))` ; list . fields . select ( 'InternalName' ). filter ( filter3 ). get (). then ( fields => { console . log ( `Application specific: ${ fields . map ( f => f . InternalName ). join ( ', ' ) } ` ); // Title, ...Custom, ContentType, Modified, Created, Author, Editor, Attachments }); // Fields in a view list . defaultView . fields . select ( 'Items' ). get (). then ( f => { const fields = ( f as any ). Items . results || ( f as any ). Items ; console . log ( `Fields in a view: ${ fields . join ( ', ' ) } ` ); }); Add Fields \u00b6 You can add fields using the add, createFieldAsXml, or one of the type specific methods. Functionally there is no difference, however one method may be easier given a certain scenario. import { sp } from \"@pnp/sp\" ; let web = sp . web ; // if you use add you _must_ include the correct FieldTypeKind in the extended properties web . fields . add ( \"MyField1\" , \"SP.FieldText\" , { Group : \"~Example\" , FieldTypeKind : 2 , Filterable : true , Hidden : false , EnforceUniqueValues : true , }). then ( f => { console . log ( f ); }); // you can also use the addText or any of the other type specific methods on the collection web . fields . addText ( \"MyField2\" , 75 , { Group : \"~Example\" }). then ( f => { console . log ( f ); }); // if you have the field schema (for example from an old elements file) you can use createFieldAsXml let xml = `` ; web . fields . createFieldAsXml ( xml ). then ( f => { console . log ( f ); }); // the same operations work on a list's fields collection web . lists . getByTitle ( \"MyList\" ). fields . addText ( \"MyField5\" , 100 ). then ( f => { console . log ( f ); }); // Create a lookup field, and a dependent lookup field web . lists . getByTitle ( \"MyList\" ). fields . addLookup ( \"MyLookup\" , \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" , \"MyLookupTargetField\" ). then ( f => { console . log ( f ); // Create the dependent lookup field return web . lists . getByTitle ( \"MyList\" ). fields . addDependentLookupField ( \"MyLookup_ID\" , f . Id , \"ID\" ); }). then ( fDep => { console . log ( fDep ); }); Adding Multiline Text Fields with FullHtml \u00b6 Because the RichTextMode property is not exposed to the clients we cannot set this value via the API directly. The work around is to use the createFieldAsXml method as shown below import { sp } from \"@pnp/sp\" ; let web = sp . web ; const fieldAddResult = await web . fields . createFieldAsXml ( `` ); Update a Field \u00b6 You can also update the properties of a field in both webs and lists, but not all properties are able to be updated after creation. You can review this list for details. import { sp } from \"@pnp/sp\" ; let web = sp . web ; web . fields . getByTitle ( \"MyField4\" ). update ({ Description : \"A new description\" , }). then ( f => { console . log ( f ); }); Update a Url/Picture Field \u00b6 When updating a URL or Picture field you need to include the __metadata descriptor as shown below. import { sp } from \"@pnp/sp\" ; const data = { \"My_Field_Name\" : { \"__metadata\" : { \"type\" : \"SP.FieldUrlValue\" }, \"Description\" : \"A Pretty picture\" , \"Url\" : \"https://tenant.sharepoint.com/sites/dev/Style%20Library/DSC_0024.JPG\" , }, }; await sp . web . lists . getByTitle ( \"MyListTitle\" ). items . getById ( 1 ). update ( data ); Delete a Field \u00b6 import { sp } from \"@pnp/sp\" ; let web = sp . web ; web . fields . getByTitle ( \"MyField4\" ). delete (). then ( f => { console . log ( f ); });","title":"Fields"},{"location":"sp/docs/fields/#pnpspfields","text":"Fields allow you to store typed information within a SharePoint list. There are many types of fields and the library seeks to simplify working with the most common types. Fields exist in both site collections (site columns) or lists (list columns) and you can add/modify/delete them at either of these levels.","title":"@pnp/sp/fields"},{"location":"sp/docs/fields/#get-fields","text":"import { sp } from \"@pnp/sp\" ; let web = sp . web ; // get all the fields in a web web . fields . get (). then ( f => { console . log ( f ); }); // you can use odata operators on the fields collection web . fields . select ( \"Title\" , \"InternalName\" , \"TypeAsString\" ). top ( 10 ). orderBy ( \"Id\" ). get (). then ( f => { console . log ( f ); }); // get all the available fields in a web (includes parent web's fields) web . availablefields . get (). then ( f => { console . log ( f ); }); // get the fields in a list web . lists . getByTitle ( \"MyList\" ). fields . get (). then ( f => { console . log ( f ); }); // you can also get individual fields using getById, getByTitle, or getByInternalNameOrTitle web . fields . getById ( \"dee9c205-2537-44d6-94e2-7c957e6ebe6e\" ). get (). then ( f => { console . log ( f ); }); web . fields . getByTitle ( \"MyField4\" ). get (). then ( f => { console . log ( f ); }); web . fields . getByInternalNameOrTitle ( \"MyField4\" ). get (). then ( f => { console . log ( f ); });","title":"Get Fields"},{"location":"sp/docs/fields/#filtering-fields","text":"Sometimes you only want a subset of fields from the collection. Below are some examples of using the filter operator with the fields collection. import { sp } from '@pnp/sp' ; const list = sp . web . lists . getByTitle ( 'Custom' ); // Fields which can be updated const filter1 = `Hidden eq false and ReadOnlyField eq false` ; list . fields . select ( 'InternalName' ). filter ( filter1 ). get (). then ( fields => { console . log ( `Can be updated: ${ fields . map ( f => f . InternalName ). join ( ', ' ) } ` ); // Title, ...Custom, ContentType, Attachments }); // Only custom field const filter2 = `Hidden eq false and CanBeDeleted eq true` ; list . fields . select ( 'InternalName' ). filter ( filter2 ). get (). then ( fields => { console . log ( `Custom fields: ${ fields . map ( f => f . InternalName ). join ( ', ' ) } ` ); // ...Custom }); // Application specific fields const includeFields = [ 'Title' , 'Author' , 'Editor' , 'Modified' , 'Created' ]; const filter3 = `Hidden eq false and (ReadOnlyField eq false or ( ${ includeFields . map ( field => `InternalName eq ' ${ field } '` ). join ( ' or ' ) } ))` ; list . fields . select ( 'InternalName' ). filter ( filter3 ). get (). then ( fields => { console . log ( `Application specific: ${ fields . map ( f => f . InternalName ). join ( ', ' ) } ` ); // Title, ...Custom, ContentType, Modified, Created, Author, Editor, Attachments }); // Fields in a view list . defaultView . fields . select ( 'Items' ). get (). then ( f => { const fields = ( f as any ). Items . results || ( f as any ). Items ; console . log ( `Fields in a view: ${ fields . join ( ', ' ) } ` ); });","title":"Filtering Fields"},{"location":"sp/docs/fields/#add-fields","text":"You can add fields using the add, createFieldAsXml, or one of the type specific methods. Functionally there is no difference, however one method may be easier given a certain scenario. import { sp } from \"@pnp/sp\" ; let web = sp . web ; // if you use add you _must_ include the correct FieldTypeKind in the extended properties web . fields . add ( \"MyField1\" , \"SP.FieldText\" , { Group : \"~Example\" , FieldTypeKind : 2 , Filterable : true , Hidden : false , EnforceUniqueValues : true , }). then ( f => { console . log ( f ); }); // you can also use the addText or any of the other type specific methods on the collection web . fields . addText ( \"MyField2\" , 75 , { Group : \"~Example\" }). then ( f => { console . log ( f ); }); // if you have the field schema (for example from an old elements file) you can use createFieldAsXml let xml = `` ; web . fields . createFieldAsXml ( xml ). then ( f => { console . log ( f ); }); // the same operations work on a list's fields collection web . lists . getByTitle ( \"MyList\" ). fields . addText ( \"MyField5\" , 100 ). then ( f => { console . log ( f ); }); // Create a lookup field, and a dependent lookup field web . lists . getByTitle ( \"MyList\" ). fields . addLookup ( \"MyLookup\" , \"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx\" , \"MyLookupTargetField\" ). then ( f => { console . log ( f ); // Create the dependent lookup field return web . lists . getByTitle ( \"MyList\" ). fields . addDependentLookupField ( \"MyLookup_ID\" , f . Id , \"ID\" ); }). then ( fDep => { console . log ( fDep ); });","title":"Add Fields"},{"location":"sp/docs/fields/#adding-multiline-text-fields-with-fullhtml","text":"Because the RichTextMode property is not exposed to the clients we cannot set this value via the API directly. The work around is to use the createFieldAsXml method as shown below import { sp } from \"@pnp/sp\" ; let web = sp . web ; const fieldAddResult = await web . fields . createFieldAsXml ( `` );","title":"Adding Multiline Text Fields with FullHtml"},{"location":"sp/docs/fields/#update-a-field","text":"You can also update the properties of a field in both webs and lists, but not all properties are able to be updated after creation. You can review this list for details. import { sp } from \"@pnp/sp\" ; let web = sp . web ; web . fields . getByTitle ( \"MyField4\" ). update ({ Description : \"A new description\" , }). then ( f => { console . log ( f ); });","title":"Update a Field"},{"location":"sp/docs/fields/#update-a-urlpicture-field","text":"When updating a URL or Picture field you need to include the __metadata descriptor as shown below. import { sp } from \"@pnp/sp\" ; const data = { \"My_Field_Name\" : { \"__metadata\" : { \"type\" : \"SP.FieldUrlValue\" }, \"Description\" : \"A Pretty picture\" , \"Url\" : \"https://tenant.sharepoint.com/sites/dev/Style%20Library/DSC_0024.JPG\" , }, }; await sp . web . lists . getByTitle ( \"MyListTitle\" ). items . getById ( 1 ). update ( data );","title":"Update a Url/Picture Field"},{"location":"sp/docs/fields/#delete-a-field","text":"import { sp } from \"@pnp/sp\" ; let web = sp . web ; web . fields . getByTitle ( \"MyField4\" ). delete (). then ( f => { console . log ( f ); });","title":"Delete a Field"},{"location":"sp/docs/files/","text":"@pnp/sp/files \u00b6 One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below. Reading Files \u00b6 Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser . import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.avi\" ). getBlob (). then (( blob : Blob ) => {}); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.avi\" ). getBuffer (). then (( buffer : ArrayBuffer ) => {}); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.json\" ). getJSON (). then (( json : any ) => {}); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.txt\" ). getText (). then (( text : string ) => {}); // all of these also work from a file object no matter how you access it sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/documents\" ). files . getByName ( \"file.txt\" ). getText (). then (( text : string ) => {}); Adding Files \u00b6 Likewise you can add files using one of two methods, add or addChunked. The second is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. declare var require : ( s : string ) => any ; import { ConsoleListener , Web , Logger , LogLevel , ODataRaw } from \"@pnp/sp\" ; import { auth } from \"./auth\" ; let $ = require ( \"jquery\" ); let siteUrl = \"https://mytenant.sharepoint.com/sites/dev\" ; // comment this out for non-node execution // auth(siteUrl); Logger . subscribe ( new ConsoleListener ()); Logger . activeLogLevel = LogLevel . Verbose ; let web = new Web ( siteUrl ); $ (() => { $ ( \"#testingdiv\" ). append ( \"\" ); $ ( \"#thebuttontodoit\" ). on ( 'click' , ( e ) => { e . preventDefault (); let input = < HTMLInputElement > document . getElementById ( \"thefileinput\" ); let file = input . files [ 0 ]; // you can adjust this number to control what size files are uploaded in chunks if ( file . size <= 10485760 ) { // small upload web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared%20Documents/test/\" ). files . add ( file . name , file , true ). then ( _ => Logger . write ( \"done\" )); } else { // large upload web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared%20Documents/test/\" ). files . addChunked ( file . name , file , data => { Logger . log ({ data : data , level : LogLevel.Verbose , message : \"progress\" }); }, true ). then ( _ => Logger . write ( \"done!\" )); } }); }); Setting Associated Item Values \u00b6 You can also update the file properties of a newly uploaded file using code similar to the below snippet: import { sp } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared%20Documents/test/\" ). files . add ( file . name , file , true ). then ( f => { f . file . getItem (). then ( item => { item . update ({ Title : \"A Title\" , OtherField : \"My Other Value\" }); }); }); Update File Content \u00b6 You can of course use similar methods to update existing files as shown below: import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/test.txt\" ). setContent ( \"New string content for the file.\" ); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/test.mp4\" ). setContentChunked ( file ); Check in, Check out, and Approve & Deny \u00b6 The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below. Check In \u00b6 Check in takes two optional arguments, comment and check in type. import { sp , CheckinType } from \"@pnp/sp\" ; // default options with empty comment and CheckinType.Major sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkin (). then ( _ => { console . log ( \"File checked in!\" ); }); // supply a comment (< 1024 chars) and using default check in type CheckinType.Major sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkin ( \"A comment\" ). then ( _ => { console . log ( \"File checked in!\" ); }); // Supply both comment and check in type sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkin ( \"A comment\" , CheckinType . Overwrite ). then ( _ => { console . log ( \"File checked in!\" ); }); Check Out \u00b6 Check out takes no arguments. import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkout (). then ( _ => { console . log ( \"File checked out!\" ); }); Approve and Deny \u00b6 You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). approve ( \"Approval Comment\" ). then ( _ => { console . log ( \"File approved!\" ); }); // deny with no comment sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). deny (). then ( _ => { console . log ( \"File denied!\" ); }); // deny with a supplied comment. sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). deny ( \"Deny comment\" ). then ( _ => { console . log ( \"File denied!\" ); }); Publish and Unpublish \u00b6 You can both publish and unpublish a file using the library. Both methods take an optional comment argument. import { sp } from \"@pnp/sp\" ; // publish with no comment sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). publish (). then ( _ => { console . log ( \"File published!\" ); }); // publish with a supplied comment. sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). publish ( \"Publish comment\" ). then ( _ => { console . log ( \"File published!\" ); }); // unpublish with no comment sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). unpublish (). then ( _ => { console . log ( \"File unpublished!\" ); }); // unpublish with a supplied comment. sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). unpublish ( \"Unpublish comment\" ). then ( _ => { console . log ( \"File unpublished!\" ); }); Advanced Upload Options \u00b6 Both the addChunked and setContentChunked methods support options beyond just supplying the file content. progress function \u00b6 A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: (data: ChunkedFileUploadProgressData) => void The data interface is: export interface ChunkedFileUploadProgressData { stage : \"starting\" | \"continue\" | \"finishing\" ; blockNumber : number ; totalBlocks : number ; chunkSize : number ; currentPointer : number ; fileSize : number ; } chunkSize \u00b6 This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts. getItem \u00b6 This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. import { sp } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem (). then ( item => { console . log ( item ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem ( \"Title\" , \"Modified\" ). then ( item => { console . log ( item ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem (). then ( item => { // you can also chain directly off this item instance item . getCurrentUserEffectivePermissions (). then ( perms => { console . log ( perms ); }); }); You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. import { sp } from \"@pnp/sp\" ; // also supports typing the objects so your type will be a union type sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem < { Id : number , Title : string } > ( \"Id\" , \"Title\" ). then ( item => { // You get intellisense and proper typing of the returned object console . log ( `Id: ${ item . Id } -- ${ item . Title } ` ); // You can also chain directly off this item instance item . getCurrentUserEffectivePermissions (). then ( perms => { console . log ( perms ); }); });","title":"Files"},{"location":"sp/docs/files/#pnpspfiles","text":"One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.","title":"@pnp/sp/files"},{"location":"sp/docs/files/#reading-files","text":"Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser . import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.avi\" ). getBlob (). then (( blob : Blob ) => {}); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.avi\" ). getBuffer (). then (( buffer : ArrayBuffer ) => {}); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.json\" ). getJSON (). then (( json : any ) => {}); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/file.txt\" ). getText (). then (( text : string ) => {}); // all of these also work from a file object no matter how you access it sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/documents\" ). files . getByName ( \"file.txt\" ). getText (). then (( text : string ) => {});","title":"Reading Files"},{"location":"sp/docs/files/#adding-files","text":"Likewise you can add files using one of two methods, add or addChunked. The second is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. declare var require : ( s : string ) => any ; import { ConsoleListener , Web , Logger , LogLevel , ODataRaw } from \"@pnp/sp\" ; import { auth } from \"./auth\" ; let $ = require ( \"jquery\" ); let siteUrl = \"https://mytenant.sharepoint.com/sites/dev\" ; // comment this out for non-node execution // auth(siteUrl); Logger . subscribe ( new ConsoleListener ()); Logger . activeLogLevel = LogLevel . Verbose ; let web = new Web ( siteUrl ); $ (() => { $ ( \"#testingdiv\" ). append ( \"\" ); $ ( \"#thebuttontodoit\" ). on ( 'click' , ( e ) => { e . preventDefault (); let input = < HTMLInputElement > document . getElementById ( \"thefileinput\" ); let file = input . files [ 0 ]; // you can adjust this number to control what size files are uploaded in chunks if ( file . size <= 10485760 ) { // small upload web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared%20Documents/test/\" ). files . add ( file . name , file , true ). then ( _ => Logger . write ( \"done\" )); } else { // large upload web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared%20Documents/test/\" ). files . addChunked ( file . name , file , data => { Logger . log ({ data : data , level : LogLevel.Verbose , message : \"progress\" }); }, true ). then ( _ => Logger . write ( \"done!\" )); } }); });","title":"Adding Files"},{"location":"sp/docs/files/#setting-associated-item-values","text":"You can also update the file properties of a newly uploaded file using code similar to the below snippet: import { sp } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared%20Documents/test/\" ). files . add ( file . name , file , true ). then ( f => { f . file . getItem (). then ( item => { item . update ({ Title : \"A Title\" , OtherField : \"My Other Value\" }); }); });","title":"Setting Associated Item Values"},{"location":"sp/docs/files/#update-file-content","text":"You can of course use similar methods to update existing files as shown below: import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/test.txt\" ). setContent ( \"New string content for the file.\" ); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/documents/test.mp4\" ). setContentChunked ( file );","title":"Update File Content"},{"location":"sp/docs/files/#check-in-check-out-and-approve-deny","text":"The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.","title":"Check in, Check out, and Approve & Deny"},{"location":"sp/docs/files/#check-in","text":"Check in takes two optional arguments, comment and check in type. import { sp , CheckinType } from \"@pnp/sp\" ; // default options with empty comment and CheckinType.Major sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkin (). then ( _ => { console . log ( \"File checked in!\" ); }); // supply a comment (< 1024 chars) and using default check in type CheckinType.Major sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkin ( \"A comment\" ). then ( _ => { console . log ( \"File checked in!\" ); }); // Supply both comment and check in type sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkin ( \"A comment\" , CheckinType . Overwrite ). then ( _ => { console . log ( \"File checked in!\" ); });","title":"Check In"},{"location":"sp/docs/files/#check-out","text":"Check out takes no arguments. import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). checkout (). then ( _ => { console . log ( \"File checked out!\" ); });","title":"Check Out"},{"location":"sp/docs/files/#approve-and-deny","text":"You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. import { sp } from \"@pnp/sp\" ; sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). approve ( \"Approval Comment\" ). then ( _ => { console . log ( \"File approved!\" ); }); // deny with no comment sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). deny (). then ( _ => { console . log ( \"File denied!\" ); }); // deny with a supplied comment. sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). deny ( \"Deny comment\" ). then ( _ => { console . log ( \"File denied!\" ); });","title":"Approve and Deny"},{"location":"sp/docs/files/#publish-and-unpublish","text":"You can both publish and unpublish a file using the library. Both methods take an optional comment argument. import { sp } from \"@pnp/sp\" ; // publish with no comment sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). publish (). then ( _ => { console . log ( \"File published!\" ); }); // publish with a supplied comment. sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). publish ( \"Publish comment\" ). then ( _ => { console . log ( \"File published!\" ); }); // unpublish with no comment sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). unpublish (). then ( _ => { console . log ( \"File unpublished!\" ); }); // unpublish with a supplied comment. sp . web . getFileByServerRelativeUrl ( \"/sites/dev/shared documents/file.txt\" ). unpublish ( \"Unpublish comment\" ). then ( _ => { console . log ( \"File unpublished!\" ); });","title":"Publish and Unpublish"},{"location":"sp/docs/files/#advanced-upload-options","text":"Both the addChunked and setContentChunked methods support options beyond just supplying the file content.","title":"Advanced Upload Options"},{"location":"sp/docs/files/#progress-function","text":"A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: (data: ChunkedFileUploadProgressData) => void The data interface is: export interface ChunkedFileUploadProgressData { stage : \"starting\" | \"continue\" | \"finishing\" ; blockNumber : number ; totalBlocks : number ; chunkSize : number ; currentPointer : number ; fileSize : number ; }","title":"progress function"},{"location":"sp/docs/files/#chunksize","text":"This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.","title":"chunkSize"},{"location":"sp/docs/files/#getitem","text":"This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. import { sp } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem (). then ( item => { console . log ( item ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem ( \"Title\" , \"Modified\" ). then ( item => { console . log ( item ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem (). then ( item => { // you can also chain directly off this item instance item . getCurrentUserEffectivePermissions (). then ( perms => { console . log ( perms ); }); }); You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. import { sp } from \"@pnp/sp\" ; // also supports typing the objects so your type will be a union type sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getItem < { Id : number , Title : string } > ( \"Id\" , \"Title\" ). then ( item => { // You get intellisense and proper typing of the returned object console . log ( `Id: ${ item . Id } -- ${ item . Title } ` ); // You can also chain directly off this item instance item . getCurrentUserEffectivePermissions (). then ( perms => { console . log ( perms ); }); });","title":"getItem"},{"location":"sp/docs/items/","text":"@pnp/sp/items \u00b6 GET \u00b6 Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions. Basic Get \u00b6 import { sp } from \"@pnp/sp\" ; // get all the items from a list sp . web . lists . getByTitle ( \"My List\" ). items . get (). then (( items : any []) => { console . log ( items ); }); // get a specific item by id sp . web . lists . getByTitle ( \"My List\" ). items . getById ( 1 ). get (). then (( item : any ) => { console . log ( item ); }); // use odata operators for more efficient queries sp . web . lists . getByTitle ( \"My List\" ). items . select ( \"Title\" , \"Description\" ). top ( 5 ). orderBy ( \"Modified\" , true ). get (). then (( items : any []) => { console . log ( items ); }); Get Paged Items \u00b6 Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. import { sp } from \"@pnp/sp\" ; // basic case to get paged items form a list let items = await sp . web . lists . getByTitle ( \"BigList\" ). items . getPaged (); // you can also provide a type for the returned values instead of any let items = await sp . web . lists . getByTitle ( \"BigList\" ). items . getPaged < { Title : string }[] > (); // the query also works with select to choose certain fields and top to set the page size let items = await sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" , \"Description\" ). top ( 50 ). getPaged < { Title : string }[] > (); // the results object will have two properties and one method: // the results property will be an array of the items returned if ( items . results . length > 0 ) { console . log ( \"We got results!\" ); for ( let i = 0 ; i < items . results . length ; i ++ ) { // type checking works here if we specify the return type console . log ( items . results [ i ]. Title ); } } // the hasNext property is used with the getNext method to handle paging // hasNext will be true so long as there are additional results if ( items . hasNext ) { // this will carry over the type specified in the original query for the results array items = await items . getNext (); console . log ( items . results . length ); } getListItemChangesSinceToken \u00b6 The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. import { sp } from \"@pnp/sp\" ; // Using RowLimit. Enables paging let changes = await sp . web . lists . getByTitle ( \"BigList\" ). getListItemChangesSinceToken ({ RowLimit : '5' }); // Use QueryOptions to make a XML-style query. // Because it's XML we need to escape special characters // Instead of & we use & in the query let changes = await sp . web . lists . getByTitle ( \"BigList\" ). getListItemChangesSinceToken ({ QueryOptions : '' }); // Get everything. Using null with ChangeToken gets everything let changes = await sp . web . lists . getByTitle ( \"BigList\" ). getListItemChangesSinceToken ({ ChangeToken : null }); Get All Items \u00b6 Added in 1.0.2 Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used. import { sp } from \"@pnp/sp\" ; // basic usage sp . web . lists . getByTitle ( \"BigList\" ). items . getAll (). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); // set page size sp . web . lists . getByTitle ( \"BigList\" ). items . getAll ( 4000 ). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); // use select and top. top will set page size and override the any value passed to getAll sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). top ( 4000 ). getAll (). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); // we can also use filter as a supported odata operation, but this will likely fail on large lists sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). filter ( \"Title eq 'Test'\" ). getAll (). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); Retrieving Lookup Fields \u00b6 When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"LookupList\" ). items . select ( \"Title\" , \"Lookup/Title\" , \"Lookup/ID\" ). expand ( \"Lookup\" ). get (). then (( items : any []) => { console . log ( items ); }); sp . web . lists . getByTitle ( \"LookupList\" ). items . getById ( 1 ). select ( \"Title\" , \"Lookup/Title\" , \"Lookup/ID\" ). expand ( \"Lookup\" ). get (). then (( item : any ) => { console . log ( item ); }); Retrieving PublishingPageImage \u00b6 The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread . Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. import { Web } from \"@pnp/sp\" ; const w = new Web ( \"https://{publishing site url}\" ); w . lists . getByTitle ( \"Pages\" ). items . select ( \"Title\" , \"FileRef\" , \"FieldValuesAsText/MetaInfo\" ) . expand ( \"FieldValuesAsText\" ) . get (). then ( r => { // look through the returned items. for ( var i = 0 ; i < r . length ; i ++ ) { // the title field value console . log ( r [ i ]. Title ); // find the value in the MetaInfo string using regex const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig . exec ( r [ i ]. FieldValuesAsText . MetaInfo ); if ( matches !== null && matches . length > 1 ) { // this wil be the value of the PublishingPageImage field console . log ( matches [ 1 ]); } } }). catch ( e => { console . error ( e ); }); Add Items \u00b6 There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object. import { sp , ItemAddResult } from \"@pnp/sp\" ; // add an item to the list sp . web . lists . getByTitle ( \"My List\" ). items . add ({ Title : \"Title\" , Description : \"Description\" }). then (( iar : ItemAddResult ) => { console . log ( iar ); }); Content Type \u00b6 You can also set the content type id when you create an item as shown in the example below: import { sp } from \"@pnp/sp\" ; sp . web . lists . getById ( \"4D5A36EA-6E84-4160-8458-65C436DB765C\" ). items . add ({ Title : \"Test 1\" , ContentTypeId : \"0x01030058FD86C279252341AB303852303E4DAF\" }); User Fields \u00b6 There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a \"results\" property and an array. Examples for both are shown below. import { sp } from \"@pnp/sp\" ; import { getGUID } from \"@pnp/core\" ; sp . web . lists . getByTitle ( \"PeopleFields\" ). items . add ({ Title : getGUID (), User1Id : 9 , // allows a single user User2Id : { results : [ 16 , 45 ] // allows multiple users } }). then ( i => { console . log ( i ); }); If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array. import { sp } from \"@pnp/sp\" ; const result = await sp . web . lists . getByTitle ( \"UserFieldList\" ). items . getById ( 1 ). validateUpdateListItem ([{ FieldName : \"UserField\" , FieldValue : JSON.stringify ([{ \"Key\" : \"i:0#.f|membership|person@tenant.com\" }]), }, { FieldName : \"Title\" , FieldValue : \"Test - Updated\" , }]); Lookup Fields \u00b6 What is said for User Fields is, in general, relevant to Lookup Fields: - Lookup Field types: - Single-valued lookup - Multiple-valued lookup - Id suffix should be appended to the end of lookup's EntityPropertyName in payloads - Numeric Ids for lookups' items should be passed as values import { sp } from \"@pnp/sp\" ; import { getGUID } from \"@pnp/core\" ; sp . web . lists . getByTitle ( \"LookupFields\" ). items . add ({ Title : getGUID (), LookupFieldId : 2 , // allows a single lookup value MuptiLookupFieldId : { results : [ 1 , 56 ] // allows multiple lookup value } }). then ( console . log ). catch ( console . log ); Add Multiple Items \u00b6 import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"rapidadd\" ); list . getListItemEntityTypeFullName (). then ( entityTypeFullName => { let batch = sp . web . createBatch (); list . items . inBatch ( batch ). add ({ Title : \"Batch 6\" }, entityTypeFullName ). then ( b => { console . log ( b ); }); list . items . inBatch ( batch ). add ({ Title : \"Batch 7\" }, entityTypeFullName ). then ( b => { console . log ( b ); }); batch . execute (). then ( d => console . log ( \"Done\" )); }); Update \u00b6 The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 1 ). update ({ Title : \"My New Title\" , Description : \"Here is a new description\" }). then ( i => { console . log ( i ); }); Getting and updating a collection using filter \u00b6 import { sp } from \"@pnp/sp\" ; // you are getting back a collection here sp . web . lists . getByTitle ( \"MyList\" ). items . top ( 1 ). filter ( \"Title eq 'A Title'\" ). get (). then (( items : any []) => { // see if we got something if ( items . length > 0 ) { sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( items [ 0 ]. Id ). update ({ Title : \"Updated Title\" , }). then ( result => { // here you will have updated the item console . log ( JSON . stringify ( result )); }); } }); Update Multiple Items \u00b6 This approach avoids multiple calls for the same list's entity type name. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"rapidupdate\" ); list . getListItemEntityTypeFullName (). then ( entityTypeFullName => { let batch = sp . web . createBatch (); // note requirement of \"*\" eTag param - or use a specific eTag value as needed list . items . getById ( 1 ). inBatch ( batch ). update ({ Title : \"Batch 6\" }, \"*\" , entityTypeFullName ). then ( b => { console . log ( b ); }); list . items . getById ( 2 ). inBatch ( batch ). update ({ Title : \"Batch 7\" }, \"*\" , entityTypeFullName ). then ( b => { console . log ( b ); }); batch . execute (). then ( d => console . log ( \"Done\" )); }); Recycle \u00b6 Sending an item to the Recycle Bin is as simple as calling the .recycle method. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 1 ). recycle (). then ( _ => {}); Delete \u00b6 Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 1 ). delete (). then ( _ => {}); Resolving field names \u00b6 It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used. The easiest way to get know EntityPropertyName is to use the following snippet: import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( '[Lists_Title]' ) . fields . select ( 'Title, EntityPropertyName' ) . filter ( `Hidden eq false and Title eq '[Field's_Display_Name]'` ) . get () . then ( response => { console . log ( response . map ( field => { return { Title : field.Title , EntityPropertyName : field.EntityPropertyName }; })); }) . catch ( console . log ); Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.","title":"List Items"},{"location":"sp/docs/items/#pnpspitems","text":"","title":"@pnp/sp/items"},{"location":"sp/docs/items/#get","text":"Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.","title":"GET"},{"location":"sp/docs/items/#basic-get","text":"import { sp } from \"@pnp/sp\" ; // get all the items from a list sp . web . lists . getByTitle ( \"My List\" ). items . get (). then (( items : any []) => { console . log ( items ); }); // get a specific item by id sp . web . lists . getByTitle ( \"My List\" ). items . getById ( 1 ). get (). then (( item : any ) => { console . log ( item ); }); // use odata operators for more efficient queries sp . web . lists . getByTitle ( \"My List\" ). items . select ( \"Title\" , \"Description\" ). top ( 5 ). orderBy ( \"Modified\" , true ). get (). then (( items : any []) => { console . log ( items ); });","title":"Basic Get"},{"location":"sp/docs/items/#get-paged-items","text":"Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. import { sp } from \"@pnp/sp\" ; // basic case to get paged items form a list let items = await sp . web . lists . getByTitle ( \"BigList\" ). items . getPaged (); // you can also provide a type for the returned values instead of any let items = await sp . web . lists . getByTitle ( \"BigList\" ). items . getPaged < { Title : string }[] > (); // the query also works with select to choose certain fields and top to set the page size let items = await sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" , \"Description\" ). top ( 50 ). getPaged < { Title : string }[] > (); // the results object will have two properties and one method: // the results property will be an array of the items returned if ( items . results . length > 0 ) { console . log ( \"We got results!\" ); for ( let i = 0 ; i < items . results . length ; i ++ ) { // type checking works here if we specify the return type console . log ( items . results [ i ]. Title ); } } // the hasNext property is used with the getNext method to handle paging // hasNext will be true so long as there are additional results if ( items . hasNext ) { // this will carry over the type specified in the original query for the results array items = await items . getNext (); console . log ( items . results . length ); }","title":"Get Paged Items"},{"location":"sp/docs/items/#getlistitemchangessincetoken","text":"The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. import { sp } from \"@pnp/sp\" ; // Using RowLimit. Enables paging let changes = await sp . web . lists . getByTitle ( \"BigList\" ). getListItemChangesSinceToken ({ RowLimit : '5' }); // Use QueryOptions to make a XML-style query. // Because it's XML we need to escape special characters // Instead of & we use & in the query let changes = await sp . web . lists . getByTitle ( \"BigList\" ). getListItemChangesSinceToken ({ QueryOptions : '' }); // Get everything. Using null with ChangeToken gets everything let changes = await sp . web . lists . getByTitle ( \"BigList\" ). getListItemChangesSinceToken ({ ChangeToken : null });","title":"getListItemChangesSinceToken"},{"location":"sp/docs/items/#get-all-items","text":"Added in 1.0.2 Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used. import { sp } from \"@pnp/sp\" ; // basic usage sp . web . lists . getByTitle ( \"BigList\" ). items . getAll (). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); // set page size sp . web . lists . getByTitle ( \"BigList\" ). items . getAll ( 4000 ). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); // use select and top. top will set page size and override the any value passed to getAll sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). top ( 4000 ). getAll (). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); }); // we can also use filter as a supported odata operation, but this will likely fail on large lists sp . web . lists . getByTitle ( \"BigList\" ). items . select ( \"Title\" ). filter ( \"Title eq 'Test'\" ). getAll (). then (( allItems : any []) => { // how many did we get console . log ( allItems . length ); });","title":"Get All Items"},{"location":"sp/docs/items/#retrieving-lookup-fields","text":"When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"LookupList\" ). items . select ( \"Title\" , \"Lookup/Title\" , \"Lookup/ID\" ). expand ( \"Lookup\" ). get (). then (( items : any []) => { console . log ( items ); }); sp . web . lists . getByTitle ( \"LookupList\" ). items . getById ( 1 ). select ( \"Title\" , \"Lookup/Title\" , \"Lookup/ID\" ). expand ( \"Lookup\" ). get (). then (( item : any ) => { console . log ( item ); });","title":"Retrieving Lookup Fields"},{"location":"sp/docs/items/#retrieving-publishingpageimage","text":"The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread . Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. import { Web } from \"@pnp/sp\" ; const w = new Web ( \"https://{publishing site url}\" ); w . lists . getByTitle ( \"Pages\" ). items . select ( \"Title\" , \"FileRef\" , \"FieldValuesAsText/MetaInfo\" ) . expand ( \"FieldValuesAsText\" ) . get (). then ( r => { // look through the returned items. for ( var i = 0 ; i < r . length ; i ++ ) { // the title field value console . log ( r [ i ]. Title ); // find the value in the MetaInfo string using regex const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig . exec ( r [ i ]. FieldValuesAsText . MetaInfo ); if ( matches !== null && matches . length > 1 ) { // this wil be the value of the PublishingPageImage field console . log ( matches [ 1 ]); } } }). catch ( e => { console . error ( e ); });","title":"Retrieving PublishingPageImage"},{"location":"sp/docs/items/#add-items","text":"There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object. import { sp , ItemAddResult } from \"@pnp/sp\" ; // add an item to the list sp . web . lists . getByTitle ( \"My List\" ). items . add ({ Title : \"Title\" , Description : \"Description\" }). then (( iar : ItemAddResult ) => { console . log ( iar ); });","title":"Add Items"},{"location":"sp/docs/items/#content-type","text":"You can also set the content type id when you create an item as shown in the example below: import { sp } from \"@pnp/sp\" ; sp . web . lists . getById ( \"4D5A36EA-6E84-4160-8458-65C436DB765C\" ). items . add ({ Title : \"Test 1\" , ContentTypeId : \"0x01030058FD86C279252341AB303852303E4DAF\" });","title":"Content Type"},{"location":"sp/docs/items/#user-fields","text":"There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a \"results\" property and an array. Examples for both are shown below. import { sp } from \"@pnp/sp\" ; import { getGUID } from \"@pnp/core\" ; sp . web . lists . getByTitle ( \"PeopleFields\" ). items . add ({ Title : getGUID (), User1Id : 9 , // allows a single user User2Id : { results : [ 16 , 45 ] // allows multiple users } }). then ( i => { console . log ( i ); }); If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array. import { sp } from \"@pnp/sp\" ; const result = await sp . web . lists . getByTitle ( \"UserFieldList\" ). items . getById ( 1 ). validateUpdateListItem ([{ FieldName : \"UserField\" , FieldValue : JSON.stringify ([{ \"Key\" : \"i:0#.f|membership|person@tenant.com\" }]), }, { FieldName : \"Title\" , FieldValue : \"Test - Updated\" , }]);","title":"User Fields"},{"location":"sp/docs/items/#lookup-fields","text":"What is said for User Fields is, in general, relevant to Lookup Fields: - Lookup Field types: - Single-valued lookup - Multiple-valued lookup - Id suffix should be appended to the end of lookup's EntityPropertyName in payloads - Numeric Ids for lookups' items should be passed as values import { sp } from \"@pnp/sp\" ; import { getGUID } from \"@pnp/core\" ; sp . web . lists . getByTitle ( \"LookupFields\" ). items . add ({ Title : getGUID (), LookupFieldId : 2 , // allows a single lookup value MuptiLookupFieldId : { results : [ 1 , 56 ] // allows multiple lookup value } }). then ( console . log ). catch ( console . log );","title":"Lookup Fields"},{"location":"sp/docs/items/#add-multiple-items","text":"import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"rapidadd\" ); list . getListItemEntityTypeFullName (). then ( entityTypeFullName => { let batch = sp . web . createBatch (); list . items . inBatch ( batch ). add ({ Title : \"Batch 6\" }, entityTypeFullName ). then ( b => { console . log ( b ); }); list . items . inBatch ( batch ). add ({ Title : \"Batch 7\" }, entityTypeFullName ). then ( b => { console . log ( b ); }); batch . execute (). then ( d => console . log ( \"Done\" )); });","title":"Add Multiple Items"},{"location":"sp/docs/items/#update","text":"The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 1 ). update ({ Title : \"My New Title\" , Description : \"Here is a new description\" }). then ( i => { console . log ( i ); });","title":"Update"},{"location":"sp/docs/items/#getting-and-updating-a-collection-using-filter","text":"import { sp } from \"@pnp/sp\" ; // you are getting back a collection here sp . web . lists . getByTitle ( \"MyList\" ). items . top ( 1 ). filter ( \"Title eq 'A Title'\" ). get (). then (( items : any []) => { // see if we got something if ( items . length > 0 ) { sp . web . lists . getByTitle ( \"MyList\" ). items . getById ( items [ 0 ]. Id ). update ({ Title : \"Updated Title\" , }). then ( result => { // here you will have updated the item console . log ( JSON . stringify ( result )); }); } });","title":"Getting and updating a collection using filter"},{"location":"sp/docs/items/#update-multiple-items","text":"This approach avoids multiple calls for the same list's entity type name. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"rapidupdate\" ); list . getListItemEntityTypeFullName (). then ( entityTypeFullName => { let batch = sp . web . createBatch (); // note requirement of \"*\" eTag param - or use a specific eTag value as needed list . items . getById ( 1 ). inBatch ( batch ). update ({ Title : \"Batch 6\" }, \"*\" , entityTypeFullName ). then ( b => { console . log ( b ); }); list . items . getById ( 2 ). inBatch ( batch ). update ({ Title : \"Batch 7\" }, \"*\" , entityTypeFullName ). then ( b => { console . log ( b ); }); batch . execute (). then ( d => console . log ( \"Done\" )); });","title":"Update Multiple Items"},{"location":"sp/docs/items/#recycle","text":"Sending an item to the Recycle Bin is as simple as calling the .recycle method. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 1 ). recycle (). then ( _ => {});","title":"Recycle"},{"location":"sp/docs/items/#delete","text":"Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. import { sp } from \"@pnp/sp\" ; let list = sp . web . lists . getByTitle ( \"MyList\" ); list . items . getById ( 1 ). delete (). then ( _ => {});","title":"Delete"},{"location":"sp/docs/items/#resolving-field-names","text":"It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used. The easiest way to get know EntityPropertyName is to use the following snippet: import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( '[Lists_Title]' ) . fields . select ( 'Title, EntityPropertyName' ) . filter ( `Hidden eq false and Title eq '[Field's_Display_Name]'` ) . get () . then ( response => { console . log ( response . map ( field => { return { Title : field.Title , EntityPropertyName : field.EntityPropertyName }; })); }) . catch ( console . log ); Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.","title":"Resolving field names"},{"location":"sp/docs/navigation-service/","text":"@pnp/sp/navigation service \u00b6 The global navigation service located at \"_api/navigation\" provides access to the SiteMapProvider instances available in a given site collection. getMenuState \u00b6 The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma seperated string of property names like: property1,property2,property3\\,containingcomma NOTE: the , seperator can be escaped using the \\ as escape character as done in the example above. The string above would split like: property1 property2 * property3,containingcomma import { sp } from \"@pnp/sp\" ; // Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. sp . navigation . getMenuState (). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error ); // Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 sp . navigation . getMenuState ( \"1002\" , 5 ). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error ); // Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 sp . navigation . getMenuState ( null , 5 , \"CurrentNavSiteMapProviderNoEncode\" ). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error ); getMenuNodeKey \u00b6 Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. import { sp } from \"@pnp/sp\" ; sp . navigation . getMenuNodeKey ( \"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\" ). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error );","title":"Navigation Service"},{"location":"sp/docs/navigation-service/#pnpspnavigation-service","text":"The global navigation service located at \"_api/navigation\" provides access to the SiteMapProvider instances available in a given site collection.","title":"@pnp/sp/navigation service"},{"location":"sp/docs/navigation-service/#getmenustate","text":"The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma seperated string of property names like: property1,property2,property3\\,containingcomma NOTE: the , seperator can be escaped using the \\ as escape character as done in the example above. The string above would split like: property1 property2 * property3,containingcomma import { sp } from \"@pnp/sp\" ; // Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. sp . navigation . getMenuState (). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error ); // Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 sp . navigation . getMenuState ( \"1002\" , 5 ). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error ); // Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 sp . navigation . getMenuState ( null , 5 , \"CurrentNavSiteMapProviderNoEncode\" ). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error );","title":"getMenuState"},{"location":"sp/docs/navigation-service/#getmenunodekey","text":"Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. import { sp } from \"@pnp/sp\" ; sp . navigation . getMenuNodeKey ( \"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\" ). then ( r => { console . log ( JSON . stringify ( r , null , 4 )); }). catch ( console . error );","title":"getMenuNodeKey"},{"location":"sp/docs/permissions/","text":"@pnp/sp - permissions \u00b6 A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables. Get Role Assignments \u00b6 This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. import { sp } from \"@pnp/sp\" ; import { Logger } from \"@pnp/logging\" ; sp . web . roleAssignments . get (). then ( roles => { Logger . writeJSON ( roles ); }); First Unique Ancestor Securable Object \u00b6 This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. import { sp } from \"@pnp/sp\" ; import { Logger } from \"@pnp/logging\" ; sp . web . firstUniqueAncestorSecurableObject . get (). then ( obj => { Logger . writeJSON ( obj ); }); User Effective Permissions \u00b6 This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. import { sp } from \"@pnp/sp\" ; import { Logger } from \"@pnp/logging\" ; sp . web . getUserEffectivePermissions ( \"i:0#.f|membership|user@site.com\" ). then ( perms => { Logger . writeJSON ( perms ); }); sp . web . getCurrentUserEffectivePermissions (). then ( perms => { Logger . writeJSON ( perms ); }); User Has Permissions \u00b6 Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. import { sp , PermissionKind } from \"@pnp/sp\" ; sp . web . userHasPermissions ( \"i:0#.f|membership|user@site.com\" , PermissionKind . ApproveItems ). then ( perms => { console . log ( perms ); }); sp . web . currentUserHasPermissions ( PermissionKind . ApproveItems ). then ( perms => { console . log ( perms ); }); Has Permissions \u00b6 If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. import { sp , PermissionKind } from \"@pnp/sp\" ; sp . web . getCurrentUserEffectivePermissions (). then ( perms => { if ( sp . web . hasPermissions ( perms , PermissionKind . AddListItems ) && sp . web . hasPermissions ( perms , PermissionKind . DeleteVersions )) { // ... } });","title":"Permissions"},{"location":"sp/docs/permissions/#pnpsp-permissions","text":"A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.","title":"@pnp/sp - permissions"},{"location":"sp/docs/permissions/#get-role-assignments","text":"This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. import { sp } from \"@pnp/sp\" ; import { Logger } from \"@pnp/logging\" ; sp . web . roleAssignments . get (). then ( roles => { Logger . writeJSON ( roles ); });","title":"Get Role Assignments"},{"location":"sp/docs/permissions/#first-unique-ancestor-securable-object","text":"This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. import { sp } from \"@pnp/sp\" ; import { Logger } from \"@pnp/logging\" ; sp . web . firstUniqueAncestorSecurableObject . get (). then ( obj => { Logger . writeJSON ( obj ); });","title":"First Unique Ancestor Securable Object"},{"location":"sp/docs/permissions/#user-effective-permissions","text":"This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. import { sp } from \"@pnp/sp\" ; import { Logger } from \"@pnp/logging\" ; sp . web . getUserEffectivePermissions ( \"i:0#.f|membership|user@site.com\" ). then ( perms => { Logger . writeJSON ( perms ); }); sp . web . getCurrentUserEffectivePermissions (). then ( perms => { Logger . writeJSON ( perms ); });","title":"User Effective Permissions"},{"location":"sp/docs/permissions/#user-has-permissions","text":"Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. import { sp , PermissionKind } from \"@pnp/sp\" ; sp . web . userHasPermissions ( \"i:0#.f|membership|user@site.com\" , PermissionKind . ApproveItems ). then ( perms => { console . log ( perms ); }); sp . web . currentUserHasPermissions ( PermissionKind . ApproveItems ). then ( perms => { console . log ( perms ); });","title":"User Has Permissions"},{"location":"sp/docs/permissions/#has-permissions","text":"If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. import { sp , PermissionKind } from \"@pnp/sp\" ; sp . web . getCurrentUserEffectivePermissions (). then ( perms => { if ( sp . web . hasPermissions ( perms , PermissionKind . AddListItems ) && sp . web . hasPermissions ( perms , PermissionKind . DeleteVersions )) { // ... } });","title":"Has Permissions"},{"location":"sp/docs/profiles/","text":"@pnp/sp/profiles \u00b6 The profile services allows to to work with the SharePoint User Profile Store. Profiles \u00b6 Profiles is accessed directly from the root sp object. import { sp } from \"@pnp/sp\" ; GET \u00b6 Get profile properties for a specific user \u00b6 getPropertiesFor(loginName: string): Promise; sp . profiles . getPropertiesFor ( loginName ). then (( profile : any ) => { console . log ( profile . DisplayName ); console . log ( profile . Email ); console . log ( profile . Title ); console . log ( profile . UserProfileProperties . length ); // Properties are stored in inconvenient Key/Value pairs, // so parse into an object called userProperties var properties = {}; profile . UserProfileProperties . forEach ( function ( prop ) { properties [ prop . Key ] = prop . Value ; }); profile . userProperties = properties ; } Get a specific property for a specific user \u00b6 getUserProfilePropertyFor(loginName: string, propertyName: string): Promise; sp . profiles . getUserProfilePropertyFor ( loginName , propName ). then (( prop : string ) => { console . log ( prop ); }; Find whether a user is following another user \u00b6 isFollowing(follower: string, followee: string): Promise; sp . profiles . isFollowing ( follower , followee ). then (( followed : boolean ) => { console . log ( followed ); }; Find out who a user is following \u00b6 getPeopleFollowedBy(loginName: string): Promise; sp . profiles . getPeopleFollowedBy ( loginName ). then (( followed : any []) => { console . log ( followed . length ); }; Find out if the current user is followed by another user \u00b6 amIFollowedBy(loginName: string): Promise; Returns a boolean indicating if the current user is followed by the user with loginName. Get a specific property for the specified user. sp . profiles . amIFollowedBy ( loginName ). then (( followed : boolean ) => { console . log ( followed ); }; Get the people who are following the specified user \u00b6 getFollowersFor(loginName: string): Promise; sp . profiles . getFollowersFor ( loginName ). then (( followed : any ) => { console . log ( followed . length ); }; SET \u00b6 Set a single value property value \u00b6 setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string) Set a user's user profile property. sp . profiles . setSingleValueProfileProperty ( accountName , propertyName , propertyValue ); Set multi valued User Profile property \u00b6 setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise; sp . profiles . setSingleValueProfileProperty ( accountName , propertyName , propertyValues ); Upload and set the user profile picture \u00b6 Users can upload a picture to their own profile only). Not supported for batching. Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB setMyProfilePic(profilePicSource: Blob): Promise;","title":"Profiles"},{"location":"sp/docs/profiles/#pnpspprofiles","text":"The profile services allows to to work with the SharePoint User Profile Store.","title":"@pnp/sp/profiles"},{"location":"sp/docs/profiles/#profiles","text":"Profiles is accessed directly from the root sp object. import { sp } from \"@pnp/sp\" ;","title":"Profiles"},{"location":"sp/docs/profiles/#get","text":"","title":"GET"},{"location":"sp/docs/profiles/#get-profile-properties-for-a-specific-user","text":"getPropertiesFor(loginName: string): Promise; sp . profiles . getPropertiesFor ( loginName ). then (( profile : any ) => { console . log ( profile . DisplayName ); console . log ( profile . Email ); console . log ( profile . Title ); console . log ( profile . UserProfileProperties . length ); // Properties are stored in inconvenient Key/Value pairs, // so parse into an object called userProperties var properties = {}; profile . UserProfileProperties . forEach ( function ( prop ) { properties [ prop . Key ] = prop . Value ; }); profile . userProperties = properties ; }","title":"Get profile properties for a specific user"},{"location":"sp/docs/profiles/#get-a-specific-property-for-a-specific-user","text":"getUserProfilePropertyFor(loginName: string, propertyName: string): Promise; sp . profiles . getUserProfilePropertyFor ( loginName , propName ). then (( prop : string ) => { console . log ( prop ); };","title":"Get a specific property for a specific user"},{"location":"sp/docs/profiles/#find-whether-a-user-is-following-another-user","text":"isFollowing(follower: string, followee: string): Promise; sp . profiles . isFollowing ( follower , followee ). then (( followed : boolean ) => { console . log ( followed ); };","title":"Find whether a user is following another user"},{"location":"sp/docs/profiles/#find-out-who-a-user-is-following","text":"getPeopleFollowedBy(loginName: string): Promise; sp . profiles . getPeopleFollowedBy ( loginName ). then (( followed : any []) => { console . log ( followed . length ); };","title":"Find out who a user is following"},{"location":"sp/docs/profiles/#find-out-if-the-current-user-is-followed-by-another-user","text":"amIFollowedBy(loginName: string): Promise; Returns a boolean indicating if the current user is followed by the user with loginName. Get a specific property for the specified user. sp . profiles . amIFollowedBy ( loginName ). then (( followed : boolean ) => { console . log ( followed ); };","title":"Find out if the current user is followed by another user"},{"location":"sp/docs/profiles/#get-the-people-who-are-following-the-specified-user","text":"getFollowersFor(loginName: string): Promise; sp . profiles . getFollowersFor ( loginName ). then (( followed : any ) => { console . log ( followed . length ); };","title":"Get the people who are following the specified user"},{"location":"sp/docs/profiles/#set","text":"","title":"SET"},{"location":"sp/docs/profiles/#set-a-single-value-property-value","text":"setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string) Set a user's user profile property. sp . profiles . setSingleValueProfileProperty ( accountName , propertyName , propertyValue );","title":"Set a single value property value"},{"location":"sp/docs/profiles/#set-multi-valued-user-profile-property","text":"setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise; sp . profiles . setSingleValueProfileProperty ( accountName , propertyName , propertyValues );","title":"Set multi valued User Profile property"},{"location":"sp/docs/profiles/#upload-and-set-the-user-profile-picture","text":"Users can upload a picture to their own profile only). Not supported for batching. Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB setMyProfilePic(profilePicSource: Blob): Promise;","title":"Upload and set the user profile picture"},{"location":"sp/docs/related-items/","text":"@pnp/sp/relateditems \u00b6 Related items are used in Task and Workflow lists (as well as others) to track items that have relationships similar to database relationships. All methods chain off the Web's relatedItems property as shown below: getRelatedItems \u00b6 Expects the named library to exist within the contextual web. import { sp , RelatedItem } from \"@pnp/sp\" ; sp . web . relatedItems . getRelatedItems ( \"Documents\" , 1 ). then (( result : RelatedItem []) => { console . log ( result ); }); getPageOneRelatedItems \u00b6 Expects the named library to exist within the contextual web. import { sp , RelatedItem } from \"@pnp/sp\" ; sp . web . relatedItems . getPageOneRelatedItems ( \"Documents\" , 1 ). then (( result : RelatedItem []) => { console . log ( result ); }); addSingleLink \u00b6 import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . addSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" ). then ( _ => { // ... return is void }); sp . web . relatedItems . addSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" , true ). then ( _ => { // ... return is void }); addSingleLinkToUrl \u00b6 Adds a related item link from an item specified by list name and item id, to an item specified by url import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . addSingleLinkToUrl ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" ). then ( _ => { // ... return is void }); sp . web . relatedItems . addSingleLinkToUrl ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" , true ). then ( _ => { // ... return is void }); addSingleLinkFromUrl \u00b6 Adds a related item link from an item specified by url, to an item specified by list name and item id import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . addSingleLinkFromUrl ( \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" , \"RelatedItemsList1\" , 2 ). then ( _ => { // ... return is void }); sp . web . relatedItems . addSingleLinkFromUrl ( \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" , \"RelatedItemsList1\" , 2 , true ). then ( _ => { // ... return is void }); deleteSingleLink \u00b6 import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . deleteSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" ). then ( _ => { // ... return is void }); sp . web . relatedItems . deleteSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" , true ). then ( _ => { // ... return is void });","title":"Related Items"},{"location":"sp/docs/related-items/#pnpsprelateditems","text":"Related items are used in Task and Workflow lists (as well as others) to track items that have relationships similar to database relationships. All methods chain off the Web's relatedItems property as shown below:","title":"@pnp/sp/relateditems"},{"location":"sp/docs/related-items/#getrelateditems","text":"Expects the named library to exist within the contextual web. import { sp , RelatedItem } from \"@pnp/sp\" ; sp . web . relatedItems . getRelatedItems ( \"Documents\" , 1 ). then (( result : RelatedItem []) => { console . log ( result ); });","title":"getRelatedItems"},{"location":"sp/docs/related-items/#getpageonerelateditems","text":"Expects the named library to exist within the contextual web. import { sp , RelatedItem } from \"@pnp/sp\" ; sp . web . relatedItems . getPageOneRelatedItems ( \"Documents\" , 1 ). then (( result : RelatedItem []) => { console . log ( result ); });","title":"getPageOneRelatedItems"},{"location":"sp/docs/related-items/#addsinglelink","text":"import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . addSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" ). then ( _ => { // ... return is void }); sp . web . relatedItems . addSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" , true ). then ( _ => { // ... return is void });","title":"addSingleLink"},{"location":"sp/docs/related-items/#addsinglelinktourl","text":"Adds a related item link from an item specified by list name and item id, to an item specified by url import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . addSingleLinkToUrl ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" ). then ( _ => { // ... return is void }); sp . web . relatedItems . addSingleLinkToUrl ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" , true ). then ( _ => { // ... return is void });","title":"addSingleLinkToUrl"},{"location":"sp/docs/related-items/#addsinglelinkfromurl","text":"Adds a related item link from an item specified by url, to an item specified by list name and item id import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . addSingleLinkFromUrl ( \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" , \"RelatedItemsList1\" , 2 ). then ( _ => { // ... return is void }); sp . web . relatedItems . addSingleLinkFromUrl ( \"https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt\" , \"RelatedItemsList1\" , 2 , true ). then ( _ => { // ... return is void });","title":"addSingleLinkFromUrl"},{"location":"sp/docs/related-items/#deletesinglelink","text":"import { sp } from \"@pnp/sp\" ; sp . web . relatedItems . deleteSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" ). then ( _ => { // ... return is void }); sp . web . relatedItems . deleteSingleLink ( \"RelatedItemsList1\" , 2 , \"https://site.sharepoint.com/sites/dev/subsite\" , \"RelatedItemsList2\" , 1 , \"https://site.sharepoint.com/sites/dev\" , true ). then ( _ => { // ... return is void });","title":"deleteSingleLink"},{"location":"sp/docs/search/","text":"@pnp/sp/search \u00b6 Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and search suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier. Search \u00b6 Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the SearchQuery interface, or a SearchQueryBuilder instance. The first two are shown below. import { sp , SearchQuery , SearchResults } from \"@pnp/sp\" ; // text search using SharePoint default values for other parameters sp . search ( \"test\" ). then (( r : SearchResults ) => { console . log ( r . ElapsedTime ); console . log ( r . RowCount ); console . log ( r . PrimarySearchResults ); }); // define a search query object matching the SearchQuery interface sp . search ( < SearchQuery > { Querytext : \"test\" , RowLimit : 10 , EnableInterleaving : true , }). then (( r : SearchResults ) => { console . log ( r . ElapsedTime ); console . log ( r . RowCount ); console . log ( r . PrimarySearchResults ); }); Search Result Caching \u00b6 Added in 1.1.5 As of version 1.1.5 you can also use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace \"search\" with \"searchWithCaching\" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options import { sp , SearchQuery , SearchResults , SearchQueryBuilder } from \"@pnp/sp\" ; sp . searchWithCaching ( < SearchQuery > { Querytext : \"test\" , RowLimit : 10 , EnableInterleaving : true , }). then (( r : SearchResults ) => { console . log ( r . ElapsedTime ); console . log ( r . RowCount ); console . log ( r . PrimarySearchResults ); }); const builder = SearchQueryBuilder (). text ( \"test\" ). rowLimit ( 3 ); // supply a search query builder and caching options sp . searchWithCaching ( builder , { key : \"mykey\" , expiration : dateAdd ( new Date (), \"month\" , 1 ) }). then ( r2 => { console . log ( r2 . TotalRows ); }); Paging with SearchResults.getPage \u00b6 Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. import { sp , SearchQueryBuilder , SearchResults } from \"@pnp/sp\" ; // this will hold our current results let currentResults : SearchResults = null ; let page = 1 ; // triggered on page load through some means function onStart() { // construct our query that will be throughout the paging process, likely from user input const q = SearchQueryBuilder . create ( \"test\" ). rowLimit ( 5 ); sp . search ( q ). then (( r : SearchResults ) => { currentResults = r ; // update the current results page = 1 ; // reset if needed // update UI with data... }); } // triggered by an event function next() { currentResults . getPage ( ++ page ). then (( r : SearchResults ) => { currentResults = r ; // update the current results // update UI with data... }); } // triggered by an event function prev() { currentResults . getPage ( -- page ). then (( r : SearchResults ) => { currentResults = r ; // update the current results // update UI with data... }); } SearchQueryBuilder \u00b6 The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. import { SearchQueryBuilder } from \"@pnp/sp\" ; // basic usage let q = SearchQueryBuilder (). text ( \"test\" ). rowLimit ( 4 ). enablePhonetic ; sp . search ( q ). then ( h => { /* ... */ }); // provide a default query text in the create() let q2 = SearchQueryBuilder ( \"text\" ). rowLimit ( 4 ). enablePhonetic ; sp . search ( q2 ). then ( h => { /* ... */ }); // provide query text and a template // shared settings across queries const appSearchSettings : SearchQuery = { EnablePhonetic : true , HiddenConstraints : \"reports\" }; let q3 = SearchQueryBuilder ( \"test\" , appSearchSettings ). enableQueryRules ; let q4 = SearchQueryBuilder ( \"financial data\" , appSearchSettings ). enableSorting . enableStemming ; sp . search ( q3 ). then ( h => { /* ... */ }); sp . search ( q4 ). then ( h => { /* ... */ }); Search Suggest \u00b6 Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches SearchSuggestQuery. import { sp , SearchSuggestQuery , SearchSuggestResult } from \"@pnp/sp\" ; sp . searchSuggest ( \"test\" ). then (( r : SearchSuggestResult ) => { console . log ( r ); }); sp . searchSuggest ( < SearchSuggestQuery > { querytext : \"test\" , count : 5 , }). then (( r : SearchSuggestResult ) => { console . log ( r ); });","title":"Search"},{"location":"sp/docs/search/#pnpspsearch","text":"Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and search suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.","title":"@pnp/sp/search"},{"location":"sp/docs/search/#search","text":"Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the SearchQuery interface, or a SearchQueryBuilder instance. The first two are shown below. import { sp , SearchQuery , SearchResults } from \"@pnp/sp\" ; // text search using SharePoint default values for other parameters sp . search ( \"test\" ). then (( r : SearchResults ) => { console . log ( r . ElapsedTime ); console . log ( r . RowCount ); console . log ( r . PrimarySearchResults ); }); // define a search query object matching the SearchQuery interface sp . search ( < SearchQuery > { Querytext : \"test\" , RowLimit : 10 , EnableInterleaving : true , }). then (( r : SearchResults ) => { console . log ( r . ElapsedTime ); console . log ( r . RowCount ); console . log ( r . PrimarySearchResults ); });","title":"Search"},{"location":"sp/docs/search/#search-result-caching","text":"Added in 1.1.5 As of version 1.1.5 you can also use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace \"search\" with \"searchWithCaching\" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options import { sp , SearchQuery , SearchResults , SearchQueryBuilder } from \"@pnp/sp\" ; sp . searchWithCaching ( < SearchQuery > { Querytext : \"test\" , RowLimit : 10 , EnableInterleaving : true , }). then (( r : SearchResults ) => { console . log ( r . ElapsedTime ); console . log ( r . RowCount ); console . log ( r . PrimarySearchResults ); }); const builder = SearchQueryBuilder (). text ( \"test\" ). rowLimit ( 3 ); // supply a search query builder and caching options sp . searchWithCaching ( builder , { key : \"mykey\" , expiration : dateAdd ( new Date (), \"month\" , 1 ) }). then ( r2 => { console . log ( r2 . TotalRows ); });","title":"Search Result Caching"},{"location":"sp/docs/search/#paging-with-searchresultsgetpage","text":"Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. import { sp , SearchQueryBuilder , SearchResults } from \"@pnp/sp\" ; // this will hold our current results let currentResults : SearchResults = null ; let page = 1 ; // triggered on page load through some means function onStart() { // construct our query that will be throughout the paging process, likely from user input const q = SearchQueryBuilder . create ( \"test\" ). rowLimit ( 5 ); sp . search ( q ). then (( r : SearchResults ) => { currentResults = r ; // update the current results page = 1 ; // reset if needed // update UI with data... }); } // triggered by an event function next() { currentResults . getPage ( ++ page ). then (( r : SearchResults ) => { currentResults = r ; // update the current results // update UI with data... }); } // triggered by an event function prev() { currentResults . getPage ( -- page ). then (( r : SearchResults ) => { currentResults = r ; // update the current results // update UI with data... }); }","title":"Paging with SearchResults.getPage"},{"location":"sp/docs/search/#searchquerybuilder","text":"The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. import { SearchQueryBuilder } from \"@pnp/sp\" ; // basic usage let q = SearchQueryBuilder (). text ( \"test\" ). rowLimit ( 4 ). enablePhonetic ; sp . search ( q ). then ( h => { /* ... */ }); // provide a default query text in the create() let q2 = SearchQueryBuilder ( \"text\" ). rowLimit ( 4 ). enablePhonetic ; sp . search ( q2 ). then ( h => { /* ... */ }); // provide query text and a template // shared settings across queries const appSearchSettings : SearchQuery = { EnablePhonetic : true , HiddenConstraints : \"reports\" }; let q3 = SearchQueryBuilder ( \"test\" , appSearchSettings ). enableQueryRules ; let q4 = SearchQueryBuilder ( \"financial data\" , appSearchSettings ). enableSorting . enableStemming ; sp . search ( q3 ). then ( h => { /* ... */ }); sp . search ( q4 ). then ( h => { /* ... */ });","title":"SearchQueryBuilder"},{"location":"sp/docs/search/#search-suggest","text":"Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches SearchSuggestQuery. import { sp , SearchSuggestQuery , SearchSuggestResult } from \"@pnp/sp\" ; sp . searchSuggest ( \"test\" ). then (( r : SearchSuggestResult ) => { console . log ( r ); }); sp . searchSuggest ( < SearchSuggestQuery > { querytext : \"test\" , count : 5 , }). then (( r : SearchSuggestResult ) => { console . log ( r ); });","title":"Search Suggest"},{"location":"sp/docs/sharing/","text":"@pnp/sp/sharing \u00b6 Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online. One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before submitting an issue. getShareLink \u00b6 Applies to: Item, Folder, File Creates a sharing link for the given resource with an optional expiration. import { sp , SharingLinkKind , ShareLinkResponse } from \"@pnp/sp\" ; import { dateAdd } from \"@pnp/core\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/folder1\" ). getShareLink ( SharingLinkKind . AnonymousView ). then ((( result : ShareLinkResponse ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/folder1\" ). getShareLink ( SharingLinkKind . AnonymousView , dateAdd ( new Date (), \"day\" , 5 )). then (( result : ShareLinkResponse ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); shareWith \u00b6 Applies to: Item, Folder, File, Web Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. import { sp , SharingResult , SharingRole } from \"@pnp/sp\" ; sp . web . shareWith ( \"i:0#.f|membership|user@site.com\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . shareWith ( \"i:0#.f|membership|user@site.com\" , SharingRole . Edit ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/folder1\" ). shareWith ( \"i:0#.f|membership|user@site.com\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). shareWith ( \"i:0#.f|membership|user@site.com\" , SharingRole . Edit , true , true ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/Shared Documents/test.txt\" ). shareWith ( \"i:0#.f|membership|user@site.com\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/Shared Documents/test.txt\" ). shareWith ( \"i:0#.f|membership|user@site.com\" , SharingRole . Edit ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); shareObject & shareObjectRaw \u00b6 Applies to: Web Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. import { sp , SharingResult , SharingRole } from \"@pnp/sp\" ; sp . web . shareObject ( \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\" , \"i:0#.f|membership|user@site.com\" , SharingRole . View ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . shareObjectRaw ({ url : \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\" , peoplePickerInput : [{ Key : \"i:0#.f|membership|user@site.com\" }], roleValue : \"role: 1973741327\" , groupId : 0 , propagateAcl : false , sendEmail : true , includeAnonymousLinkInEmail : false , emailSubject : \"subject\" , emailBody : \"body\" , useSimplifiedRoles : true , }); unshareObject \u00b6 Applies to: Web import { sp , SharingResult } from \"@pnp/sp\" ; sp . web . unshareObject ( \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); checkSharingPermissions \u00b6 Applies to: Item, Folder, File Checks Permissions on the list of Users and returns back role the users have on the Item. import { sp , SharingEntityPermission } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). checkSharingPermissions ([{ alias : \"i:0#.f|membership|user@site.com\" }]). then (( result : SharingEntityPermission []) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); getSharingInformation \u00b6 Applies to: Item, Folder, File Get Sharing Information. import { sp , SharingInformation } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getSharingInformation (). then (( result : SharingInformation ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); getObjectSharingSettings \u00b6 Applies to: Item, Folder, File Gets the sharing settings import { sp , ObjectSharingSettings } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getObjectSharingSettings (). then (( result : ObjectSharingSettings ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); unshare \u00b6 Applies to: Item, Folder, File Unshares a given resource import { sp , SharingResult } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). unshare (). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); deleteSharingLinkByKind \u00b6 Applies to: Item, Folder, File import { sp , SharingLinkKind , SharingResult } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). deleteSharingLinkByKind ( SharingLinkKind . AnonymousEdit ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); unshareLink \u00b6 Applies to: Item, Folder, File import { sp , SharingLinkKind } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). unshareLink ( SharingLinkKind . AnonymousEdit ). then ( _ => { console . log ( \"done\" ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). unshareLink ( SharingLinkKind . AnonymousEdit , \"12345\" ). then ( _ => { console . log ( \"done\" ); }). catch ( e => { console . error ( e ); });","title":"Sharing"},{"location":"sp/docs/sharing/#pnpspsharing","text":"Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online. One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before submitting an issue.","title":"@pnp/sp/sharing"},{"location":"sp/docs/sharing/#getsharelink","text":"Applies to: Item, Folder, File Creates a sharing link for the given resource with an optional expiration. import { sp , SharingLinkKind , ShareLinkResponse } from \"@pnp/sp\" ; import { dateAdd } from \"@pnp/core\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/folder1\" ). getShareLink ( SharingLinkKind . AnonymousView ). then ((( result : ShareLinkResponse ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/folder1\" ). getShareLink ( SharingLinkKind . AnonymousView , dateAdd ( new Date (), \"day\" , 5 )). then (( result : ShareLinkResponse ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"getShareLink"},{"location":"sp/docs/sharing/#sharewith","text":"Applies to: Item, Folder, File, Web Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. import { sp , SharingResult , SharingRole } from \"@pnp/sp\" ; sp . web . shareWith ( \"i:0#.f|membership|user@site.com\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . shareWith ( \"i:0#.f|membership|user@site.com\" , SharingRole . Edit ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/folder1\" ). shareWith ( \"i:0#.f|membership|user@site.com\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). shareWith ( \"i:0#.f|membership|user@site.com\" , SharingRole . Edit , true , true ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/Shared Documents/test.txt\" ). shareWith ( \"i:0#.f|membership|user@site.com\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . getFileByServerRelativeUrl ( \"/sites/dev/Shared Documents/test.txt\" ). shareWith ( \"i:0#.f|membership|user@site.com\" , SharingRole . Edit ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"shareWith"},{"location":"sp/docs/sharing/#shareobject-shareobjectraw","text":"Applies to: Web Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. import { sp , SharingResult , SharingRole } from \"@pnp/sp\" ; sp . web . shareObject ( \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\" , \"i:0#.f|membership|user@site.com\" , SharingRole . View ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); }); sp . web . shareObjectRaw ({ url : \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\" , peoplePickerInput : [{ Key : \"i:0#.f|membership|user@site.com\" }], roleValue : \"role: 1973741327\" , groupId : 0 , propagateAcl : false , sendEmail : true , includeAnonymousLinkInEmail : false , emailSubject : \"subject\" , emailBody : \"body\" , useSimplifiedRoles : true , });","title":"shareObject & shareObjectRaw"},{"location":"sp/docs/sharing/#unshareobject","text":"Applies to: Web import { sp , SharingResult } from \"@pnp/sp\" ; sp . web . unshareObject ( \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\" ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"unshareObject"},{"location":"sp/docs/sharing/#checksharingpermissions","text":"Applies to: Item, Folder, File Checks Permissions on the list of Users and returns back role the users have on the Item. import { sp , SharingEntityPermission } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). checkSharingPermissions ([{ alias : \"i:0#.f|membership|user@site.com\" }]). then (( result : SharingEntityPermission []) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"checkSharingPermissions"},{"location":"sp/docs/sharing/#getsharinginformation","text":"Applies to: Item, Folder, File Get Sharing Information. import { sp , SharingInformation } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getSharingInformation (). then (( result : SharingInformation ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"getSharingInformation"},{"location":"sp/docs/sharing/#getobjectsharingsettings","text":"Applies to: Item, Folder, File Gets the sharing settings import { sp , ObjectSharingSettings } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). getObjectSharingSettings (). then (( result : ObjectSharingSettings ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"getObjectSharingSettings"},{"location":"sp/docs/sharing/#unshare","text":"Applies to: Item, Folder, File Unshares a given resource import { sp , SharingResult } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). unshare (). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"unshare"},{"location":"sp/docs/sharing/#deletesharinglinkbykind","text":"Applies to: Item, Folder, File import { sp , SharingLinkKind , SharingResult } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). deleteSharingLinkByKind ( SharingLinkKind . AnonymousEdit ). then (( result : SharingResult ) => { console . log ( result ); }). catch ( e => { console . error ( e ); });","title":"deleteSharingLinkByKind"},{"location":"sp/docs/sharing/#unsharelink","text":"Applies to: Item, Folder, File import { sp , SharingLinkKind } from \"@pnp/sp\" ; sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). unshareLink ( SharingLinkKind . AnonymousEdit ). then ( _ => { console . log ( \"done\" ); }). catch ( e => { console . error ( e ); }); sp . web . getFolderByServerRelativeUrl ( \"/sites/dev/Shared Documents/test\" ). unshareLink ( SharingLinkKind . AnonymousEdit , \"12345\" ). then ( _ => { console . log ( \"done\" ); }). catch ( e => { console . error ( e ); });","title":"unshareLink"},{"location":"sp/docs/sitedesigns/","text":"@pnp/sp/sitedesigns \u00b6 You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information. Site Designs \u00b6 Create a new site design \u00b6 import { sp } from \"@pnp/sp\" ; // WebTemplate: 64 Team site template, 68 Communication site template const siteDesign = await sp . siteDesigns . createSiteDesign ({ SiteScriptIds : [ \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" ], Title : \"SiteDesign001\" , WebTemplate : \"64\" , }); console . log ( siteDesign . Title ); Applying a site design to a site \u00b6 import { sp } from \"@pnp/sp\" ; // Limited to 30 actions in a site script, but runs synchronously await sp . siteDesigns . applySiteDesign ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , \"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\" ); // Better use the following method for 300 actions in a site script const task = await sp . web . addSiteDesignTask ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); Retrieval \u00b6 import { sp } from \"@pnp/sp\" ; // Retrieving all site designs const allSiteDesigns = await sp . siteDesigns . getSiteDesigns (); console . log ( `Total site designs: ${ allSiteDesigns . length } ` ); // Retrieving a single site design by Id const siteDesign = await sp . siteDesigns . getSiteDesignMetadata ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); console . log ( siteDesign . Title ); Update and delete \u00b6 import { sp } from \"@pnp/sp\" ; // Update const updatedSiteDesign = await sp . siteDesigns . updateSiteDesign ({ Id : \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , Title : \"SiteDesignUpdatedTitle001\" }); // Delete await sp . siteDesigns . deleteSiteDesign ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); Setting Rights/Permissions \u00b6 import { sp } from \"@pnp/sp\" ; // Get const rights = await sp . siteDesigns . getSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); console . log ( rights . length > 0 ? rights [ 0 ]. PrincipalName : \"\" ); // Grant await sp . siteDesigns . grantSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , [ \"user@contoso.onmicrosoft.com\" ]); // Revoke await sp . siteDesigns . revokeSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , [ \"user@contoso.onmicrosoft.com\" ]); // Reset all view rights const rights = await sp . siteDesigns . getSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); await sp . siteDesigns . revokeSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , rights . map ( u => u . PrincipalName )); Get a history of site designs that have run on a web \u00b6 import { sp } from \"@pnp/sp\" ; const runs = await sp . web . getSiteDesignRuns (); const runs2 = await sp . siteDesigns . getSiteDesignRun ( \"https://TENANT.sharepoint.com/sites/mysite\" ); // Get runs specific to a site design const runs3 = await sp . web . getSiteDesignRuns ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); const runs4 = await sp . siteDesigns . getSiteDesignRun ( \"https://TENANT.sharepoint.com/sites/mysite\" , \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); // For more information about the site script actions const runStatus = await sp . web . getSiteDesignRunStatus ( runs [ 0 ]. ID ); const runStatus2 = await sp . siteDesigns . getSiteDesignRunStatus ( \"https://TENANT.sharepoint.com/sites/mysite\" , runs [ 0 ]. ID ); Site Scripts \u00b6 Create a new site script \u00b6 import { sp } from \"@pnp/sp\" ; const sitescriptContent = { \"$schema\" : \"schema.json\" , \"actions\" : [ { \"themeName\" : \"Theme Name 123\" , \"verb\" : \"applyTheme\" , }, ], \"bindata\" : {}, \"version\" : 1 , }; const siteScript = await sp . siteScripts . createSiteScript ( \"Title\" , \"description\" , sitescriptContent ); console . log ( siteScript . Title ); Retrieval \u00b6 import { sp } from \"@pnp/sp\" ; // Retrieving all site scripts const allSiteScripts = await sp . siteScripts . getSiteScripts (); console . log ( allSiteScripts . length > 0 ? allSiteScripts [ 0 ]. Title : \"\" ); // Retrieving a single site script by Id const siteScript = await sp . siteScripts . getSiteScriptMetadata ( \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" ); console . log ( siteScript . Title ); Update and delete \u00b6 import { sp } from \"@pnp/sp\" ; // Update const updatedSiteScript = await sp . siteScripts . updateSiteScript ({ Id : \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" , Title : \"New Title\" }); console . log ( updatedSiteScript . Title ); // Delete await sp . siteScripts . deleteSiteScript ( \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" ); Get site script from a list \u00b6 import { sp } from \"@pnp/sp\" ; // Using the absolute URL of the list const ss = await sp . siteScripts . getSiteScriptFromList ( \"https://TENANT.sharepoint.com/Lists/mylist\" ); // Using the PnPjs web object to fetch the site script from a specific list const ss2 = await sp . web . lists . getByTitle ( \"mylist\" ). getSiteScript (); Get site script from a web \u00b6 import { sp } from \"@pnp/sp\" ; const extractInfo = { IncludeBranding : true , IncludeLinksToExportedItems : true , IncludeRegionalSettings : true , IncludeSiteExternalSharingCapability : true , IncludeTheme : true , IncludedLists : [ \"Lists/MyList\" ] }; const ss = await sp . siteScripts . getSiteScriptFromWeb ( \"https://TENANT.sharepoint.com/sites/mysite\" , extractInfo ); // Using the PnPjs web object to fetch the site script from a specific web const ss2 = await sp . web . getSiteScript ( extractInfo );","title":"Site Designs"},{"location":"sp/docs/sitedesigns/#pnpspsitedesigns","text":"You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information.","title":"@pnp/sp/sitedesigns"},{"location":"sp/docs/sitedesigns/#site-designs","text":"","title":"Site Designs"},{"location":"sp/docs/sitedesigns/#create-a-new-site-design","text":"import { sp } from \"@pnp/sp\" ; // WebTemplate: 64 Team site template, 68 Communication site template const siteDesign = await sp . siteDesigns . createSiteDesign ({ SiteScriptIds : [ \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" ], Title : \"SiteDesign001\" , WebTemplate : \"64\" , }); console . log ( siteDesign . Title );","title":"Create a new site design"},{"location":"sp/docs/sitedesigns/#applying-a-site-design-to-a-site","text":"import { sp } from \"@pnp/sp\" ; // Limited to 30 actions in a site script, but runs synchronously await sp . siteDesigns . applySiteDesign ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , \"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\" ); // Better use the following method for 300 actions in a site script const task = await sp . web . addSiteDesignTask ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" );","title":"Applying a site design to a site"},{"location":"sp/docs/sitedesigns/#retrieval","text":"import { sp } from \"@pnp/sp\" ; // Retrieving all site designs const allSiteDesigns = await sp . siteDesigns . getSiteDesigns (); console . log ( `Total site designs: ${ allSiteDesigns . length } ` ); // Retrieving a single site design by Id const siteDesign = await sp . siteDesigns . getSiteDesignMetadata ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); console . log ( siteDesign . Title );","title":"Retrieval"},{"location":"sp/docs/sitedesigns/#update-and-delete","text":"import { sp } from \"@pnp/sp\" ; // Update const updatedSiteDesign = await sp . siteDesigns . updateSiteDesign ({ Id : \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , Title : \"SiteDesignUpdatedTitle001\" }); // Delete await sp . siteDesigns . deleteSiteDesign ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" );","title":"Update and delete"},{"location":"sp/docs/sitedesigns/#setting-rightspermissions","text":"import { sp } from \"@pnp/sp\" ; // Get const rights = await sp . siteDesigns . getSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); console . log ( rights . length > 0 ? rights [ 0 ]. PrincipalName : \"\" ); // Grant await sp . siteDesigns . grantSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , [ \"user@contoso.onmicrosoft.com\" ]); // Revoke await sp . siteDesigns . revokeSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , [ \"user@contoso.onmicrosoft.com\" ]); // Reset all view rights const rights = await sp . siteDesigns . getSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); await sp . siteDesigns . revokeSiteDesignRights ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" , rights . map ( u => u . PrincipalName ));","title":"Setting Rights/Permissions"},{"location":"sp/docs/sitedesigns/#get-a-history-of-site-designs-that-have-run-on-a-web","text":"import { sp } from \"@pnp/sp\" ; const runs = await sp . web . getSiteDesignRuns (); const runs2 = await sp . siteDesigns . getSiteDesignRun ( \"https://TENANT.sharepoint.com/sites/mysite\" ); // Get runs specific to a site design const runs3 = await sp . web . getSiteDesignRuns ( \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); const runs4 = await sp . siteDesigns . getSiteDesignRun ( \"https://TENANT.sharepoint.com/sites/mysite\" , \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\" ); // For more information about the site script actions const runStatus = await sp . web . getSiteDesignRunStatus ( runs [ 0 ]. ID ); const runStatus2 = await sp . siteDesigns . getSiteDesignRunStatus ( \"https://TENANT.sharepoint.com/sites/mysite\" , runs [ 0 ]. ID );","title":"Get a history of site designs that have run on a web"},{"location":"sp/docs/sitedesigns/#site-scripts","text":"","title":"Site Scripts"},{"location":"sp/docs/sitedesigns/#create-a-new-site-script","text":"import { sp } from \"@pnp/sp\" ; const sitescriptContent = { \"$schema\" : \"schema.json\" , \"actions\" : [ { \"themeName\" : \"Theme Name 123\" , \"verb\" : \"applyTheme\" , }, ], \"bindata\" : {}, \"version\" : 1 , }; const siteScript = await sp . siteScripts . createSiteScript ( \"Title\" , \"description\" , sitescriptContent ); console . log ( siteScript . Title );","title":"Create a new site script"},{"location":"sp/docs/sitedesigns/#retrieval_1","text":"import { sp } from \"@pnp/sp\" ; // Retrieving all site scripts const allSiteScripts = await sp . siteScripts . getSiteScripts (); console . log ( allSiteScripts . length > 0 ? allSiteScripts [ 0 ]. Title : \"\" ); // Retrieving a single site script by Id const siteScript = await sp . siteScripts . getSiteScriptMetadata ( \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" ); console . log ( siteScript . Title );","title":"Retrieval"},{"location":"sp/docs/sitedesigns/#update-and-delete_1","text":"import { sp } from \"@pnp/sp\" ; // Update const updatedSiteScript = await sp . siteScripts . updateSiteScript ({ Id : \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" , Title : \"New Title\" }); console . log ( updatedSiteScript . Title ); // Delete await sp . siteScripts . deleteSiteScript ( \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\" );","title":"Update and delete"},{"location":"sp/docs/sitedesigns/#get-site-script-from-a-list","text":"import { sp } from \"@pnp/sp\" ; // Using the absolute URL of the list const ss = await sp . siteScripts . getSiteScriptFromList ( \"https://TENANT.sharepoint.com/Lists/mylist\" ); // Using the PnPjs web object to fetch the site script from a specific list const ss2 = await sp . web . lists . getByTitle ( \"mylist\" ). getSiteScript ();","title":"Get site script from a list"},{"location":"sp/docs/sitedesigns/#get-site-script-from-a-web","text":"import { sp } from \"@pnp/sp\" ; const extractInfo = { IncludeBranding : true , IncludeLinksToExportedItems : true , IncludeRegionalSettings : true , IncludeSiteExternalSharingCapability : true , IncludeTheme : true , IncludedLists : [ \"Lists/MyList\" ] }; const ss = await sp . siteScripts . getSiteScriptFromWeb ( \"https://TENANT.sharepoint.com/sites/mysite\" , extractInfo ); // Using the PnPjs web object to fetch the site script from a specific web const ss2 = await sp . web . getSiteScript ( extractInfo );","title":"Get site script from a web"},{"location":"sp/docs/sites/","text":"@pnp/sp/site - Site properties \u00b6 Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types. Get context information for the current site collection \u00b6 Using the library, you can get the context information of the current site collection import { sp } from \"@pnp/sp\" ; sp . site . getContextInfo (). then ( d => { console . log ( d . FormDigestValue ); }); Get document libraries of a web \u00b6 Using the library, you can get a list of the document libraries present in the a given web. Note: Works only in SharePoint online import { sp } from \"@pnp/sp\" ; sp . site . getDocumentLibraries ( \"https://tenant.sharepoint.com/sites/test/subsite\" ). then (( d : DocumentLibraryInformation []) => { // iterate over the array of doc lib }); Open Web By Id \u00b6 Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. sp . site . openWebById ( \"111ca453-90f5-482e-a381-cee1ff383c9e\" ). then ( w => { //we got all the data from the web as well console . log ( w . data ); // we can chain w . web . select ( \"Title\" ). get (). then ( w2 => { // ... }); }); Get site collection url from page \u00b6 Using the library, you can get the site collection url by providing a page url import { sp } from \"@pnp/sp\" ; sp . site . getWebUrlFromPageUrl ( \"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\" ). then ( d => { console . log ( d ); }); Join a hub site \u00b6 Added in 1.2.4 Note: Works only in SharePoint online Join the current site collection to a hub site collection import { sp , Site } from \"@pnp/sp\" ; var site = new Site ( \"https://tenant.sharepoint.com/sites/HubSite/\" ); var hubSiteID = \"\" ; site . select ( \"ID\" ). get (). then ( d => { // get ID of the hub site collection hubSiteID = d . Id ; // associate the current site collection the hub site collection sp . site . joinHubSite ( hubSiteID ). then ( d => { console . log ( d ); }); }); Disassociate the current site collection from a hub site collection \u00b6 Added in 1.2.4 Note: Works only in SharePoint online import { sp } from \"@pnp/sp\" ; sp . site . joinHubSite ( \"00000000-0000-0000-0000-000000000000\" ). then ( d => { console . log ( d ); }); Register a hub site \u00b6 Added in 1.2.4 Note: Works only in SharePoint online Registers the current site collection as a hub site collection import { sp } from \"@pnp/sp\" ; sp . site . registerHubSite (). then ( d => { console . log ( d ); }); Un-Register a hub site \u00b6 Added in 1.2.4 Note: Works only in SharePoint online Un-Registers the current site collection as a hub site collection import { sp } from \"@pnp/sp\" ; sp . site . unRegisterHubSite (). then ( d => { console . log ( d ); }); Create a modern communication site \u00b6 Added in 1.2.6 Note: Works only in SharePoint online Creates a modern communication site. Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com import { sp } from \"@pnp/sp\" ; const s = await sp . site . createCommunicationSite ( \"Title\" , 1033 , true , \"https://tenant.sharepoint.com/sites/commSite\" , \"Description\" , \"HBI\" , \"f6cc5403-0d63-442e-96c0-285923709ffc\" , \"a00ec589-ea9f-4dba-a34e-67e78d41e509\" , \"user@TENANT.onmicrosoft.com\" ); Create a modern team site \u00b6 Added in 1.2.6 Note: Works only in SharePoint online. It wont work with App only tokens Creates a modern team site backed by O365 group. Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site import { sp } from \"@pnp/sp\" ; sp . site . createModernTeamSite ( \"displayName\" , \"alias\" , true , 1033 , \"description\" , \"HBI\" , [ \"user1@tenant.onmicrosoft.com\" , \"user2@tenant.onmicrosoft.com\" , \"user3@tenant.onmicrosoft.com\" ], \"a00ec589-ea9f-4dba-a34e-67e78d41e509\" ) . then ( d => { console . log ( d ); }); Delete a site collection \u00b6 import { sp } from \"@pnp/sp\" ; // Delete the current site await sp . site . delete (); // Specify which site to delete const siteUrl = \"https://tenant.sharepoint.com/sites/tstpnpsitecoldelete5\" ; const site2 = new Site ( siteUrl ); await site2 . delete ();","title":"Sites"},{"location":"sp/docs/sites/#pnpspsite-site-properties","text":"Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.","title":"@pnp/sp/site - Site properties"},{"location":"sp/docs/sites/#get-context-information-for-the-current-site-collection","text":"Using the library, you can get the context information of the current site collection import { sp } from \"@pnp/sp\" ; sp . site . getContextInfo (). then ( d => { console . log ( d . FormDigestValue ); });","title":"Get context information for the current site collection"},{"location":"sp/docs/sites/#get-document-libraries-of-a-web","text":"Using the library, you can get a list of the document libraries present in the a given web. Note: Works only in SharePoint online import { sp } from \"@pnp/sp\" ; sp . site . getDocumentLibraries ( \"https://tenant.sharepoint.com/sites/test/subsite\" ). then (( d : DocumentLibraryInformation []) => { // iterate over the array of doc lib });","title":"Get document libraries of a web"},{"location":"sp/docs/sites/#open-web-by-id","text":"Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. sp . site . openWebById ( \"111ca453-90f5-482e-a381-cee1ff383c9e\" ). then ( w => { //we got all the data from the web as well console . log ( w . data ); // we can chain w . web . select ( \"Title\" ). get (). then ( w2 => { // ... }); });","title":"Open Web By Id"},{"location":"sp/docs/sites/#get-site-collection-url-from-page","text":"Using the library, you can get the site collection url by providing a page url import { sp } from \"@pnp/sp\" ; sp . site . getWebUrlFromPageUrl ( \"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\" ). then ( d => { console . log ( d ); });","title":"Get site collection url from page"},{"location":"sp/docs/sites/#join-a-hub-site","text":"Added in 1.2.4 Note: Works only in SharePoint online Join the current site collection to a hub site collection import { sp , Site } from \"@pnp/sp\" ; var site = new Site ( \"https://tenant.sharepoint.com/sites/HubSite/\" ); var hubSiteID = \"\" ; site . select ( \"ID\" ). get (). then ( d => { // get ID of the hub site collection hubSiteID = d . Id ; // associate the current site collection the hub site collection sp . site . joinHubSite ( hubSiteID ). then ( d => { console . log ( d ); }); });","title":"Join a hub site"},{"location":"sp/docs/sites/#disassociate-the-current-site-collection-from-a-hub-site-collection","text":"Added in 1.2.4 Note: Works only in SharePoint online import { sp } from \"@pnp/sp\" ; sp . site . joinHubSite ( \"00000000-0000-0000-0000-000000000000\" ). then ( d => { console . log ( d ); });","title":"Disassociate the current site collection from a hub site collection"},{"location":"sp/docs/sites/#register-a-hub-site","text":"Added in 1.2.4 Note: Works only in SharePoint online Registers the current site collection as a hub site collection import { sp } from \"@pnp/sp\" ; sp . site . registerHubSite (). then ( d => { console . log ( d ); });","title":"Register a hub site"},{"location":"sp/docs/sites/#un-register-a-hub-site","text":"Added in 1.2.4 Note: Works only in SharePoint online Un-Registers the current site collection as a hub site collection import { sp } from \"@pnp/sp\" ; sp . site . unRegisterHubSite (). then ( d => { console . log ( d ); });","title":"Un-Register a hub site"},{"location":"sp/docs/sites/#create-a-modern-communication-site","text":"Added in 1.2.6 Note: Works only in SharePoint online Creates a modern communication site. Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com import { sp } from \"@pnp/sp\" ; const s = await sp . site . createCommunicationSite ( \"Title\" , 1033 , true , \"https://tenant.sharepoint.com/sites/commSite\" , \"Description\" , \"HBI\" , \"f6cc5403-0d63-442e-96c0-285923709ffc\" , \"a00ec589-ea9f-4dba-a34e-67e78d41e509\" , \"user@TENANT.onmicrosoft.com\" );","title":"Create a modern communication site"},{"location":"sp/docs/sites/#create-a-modern-team-site","text":"Added in 1.2.6 Note: Works only in SharePoint online. It wont work with App only tokens Creates a modern team site backed by O365 group. Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site import { sp } from \"@pnp/sp\" ; sp . site . createModernTeamSite ( \"displayName\" , \"alias\" , true , 1033 , \"description\" , \"HBI\" , [ \"user1@tenant.onmicrosoft.com\" , \"user2@tenant.onmicrosoft.com\" , \"user3@tenant.onmicrosoft.com\" ], \"a00ec589-ea9f-4dba-a34e-67e78d41e509\" ) . then ( d => { console . log ( d ); });","title":"Create a modern team site"},{"location":"sp/docs/sites/#delete-a-site-collection","text":"import { sp } from \"@pnp/sp\" ; // Delete the current site await sp . site . delete (); // Specify which site to delete const siteUrl = \"https://tenant.sharepoint.com/sites/tstpnpsitecoldelete5\" ; const site2 = new Site ( siteUrl ); await site2 . delete ();","title":"Delete a site collection"},{"location":"sp/docs/social/","text":"@pnp/sp/social \u00b6 The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions. getFollowedSitesUri \u00b6 Gets a URI to a site that lists the current user's followed sites. import { sp } from \"@pnp/sp\" ; const uri = await sp . social . getFollowedSitesUri (); getFollowedDocumentsUri \u00b6 Gets a URI to a site that lists the current user's followed documents. import { sp } from \"@pnp/sp\" ; const uri = await sp . social . getFollowedDocumentsUri (); follow \u00b6 Makes the current user start following a user, document, site, or tag import { sp , SocialActorType } from \"@pnp/sp\" ; // follow a site const r1 = await sp . social . follow ({ ActorType : SocialActorType.Site , ContentUri : \"htts://tenant.sharepoint.com/sites/site\" , }); // follow a person const r2 = await sp . social . follow ({ AccountName : \"i:0#.f|membership|person@tenant.com\" , ActorType : SocialActorType.User , }); // follow a doc const r3 = await sp . social . follow ({ ActorType : SocialActorType.Document , ContentUri : \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\" , }); // follow a tag // You need the tag GUID to start following a tag. // You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. // See How to get a tag's GUID based on the tag's name by using the JavaScript object model. // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid const r4 = await sp . social . follow ({ ActorType : SocialActorType.Tag , TagGuid : \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\" , }); isFollowed \u00b6 Indicates whether the current user is following a specified user, document, site, or tag import { sp , SocialActorType } from \"@pnp/sp\" ; // pass the same social actor struct as shown in follow example for each type const r = await sp . social . isFollowed ({ AccountName : \"i:0#.f|membership|person@tenant.com\" , ActorType : SocialActorType.User , }); stopFollowing \u00b6 Makes the current user stop following a user, document, site, or tag import { sp , SocialActorType } from \"@pnp/sp\" ; // pass the same social actor struct as shown in follow example for each type const r = await sp . social . stopFollowing ({ AccountName : \"i:0#.f|membership|person@tenant.com\" , ActorType : SocialActorType.User , }); my \u00b6 get \u00b6 Gets this user's social information import { sp } from \"@pnp/sp\" ; const r = await sp . social . my . get (); followed \u00b6 Gets users, documents, sites, and tags that the current user is following based on the supplied flags. import { sp , SocialActorTypes } from \"@pnp/sp\" ; // get all the followed documents const r1 = await sp . social . my . followed ( SocialActorTypes . Document ); // get all the followed documents and sites const r2 = await sp . social . my . followed ( SocialActorTypes . Document | SocialActorTypes . Site ); // get all the followed sites updated in the last 24 hours const r3 = await sp . social . my . followed ( SocialActorTypes . Site | SocialActorTypes . WithinLast24Hours ); followedCount \u00b6 Works as followed but returns on the count of actors specifed by the query import { sp , SocialActorTypes } from \"@pnp/sp\" ; // get the followed documents count const r = await sp . social . my . followedCount ( SocialActorTypes . Document ); followers \u00b6 Gets the users who are following the current user. import { sp } from \"@pnp/sp\" ; // get the followed documents count const r = await sp . social . my . followers (); suggestions \u00b6 Gets users who the current user might want to follow. import { sp } from \"@pnp/sp\" ; // get the followed documents count const r = await sp . social . my . suggestions ();","title":"Social"},{"location":"sp/docs/social/#pnpspsocial","text":"The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions.","title":"@pnp/sp/social"},{"location":"sp/docs/social/#getfollowedsitesuri","text":"Gets a URI to a site that lists the current user's followed sites. import { sp } from \"@pnp/sp\" ; const uri = await sp . social . getFollowedSitesUri ();","title":"getFollowedSitesUri"},{"location":"sp/docs/social/#getfolloweddocumentsuri","text":"Gets a URI to a site that lists the current user's followed documents. import { sp } from \"@pnp/sp\" ; const uri = await sp . social . getFollowedDocumentsUri ();","title":"getFollowedDocumentsUri"},{"location":"sp/docs/social/#follow","text":"Makes the current user start following a user, document, site, or tag import { sp , SocialActorType } from \"@pnp/sp\" ; // follow a site const r1 = await sp . social . follow ({ ActorType : SocialActorType.Site , ContentUri : \"htts://tenant.sharepoint.com/sites/site\" , }); // follow a person const r2 = await sp . social . follow ({ AccountName : \"i:0#.f|membership|person@tenant.com\" , ActorType : SocialActorType.User , }); // follow a doc const r3 = await sp . social . follow ({ ActorType : SocialActorType.Document , ContentUri : \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\" , }); // follow a tag // You need the tag GUID to start following a tag. // You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. // See How to get a tag's GUID based on the tag's name by using the JavaScript object model. // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid const r4 = await sp . social . follow ({ ActorType : SocialActorType.Tag , TagGuid : \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\" , });","title":"follow"},{"location":"sp/docs/social/#isfollowed","text":"Indicates whether the current user is following a specified user, document, site, or tag import { sp , SocialActorType } from \"@pnp/sp\" ; // pass the same social actor struct as shown in follow example for each type const r = await sp . social . isFollowed ({ AccountName : \"i:0#.f|membership|person@tenant.com\" , ActorType : SocialActorType.User , });","title":"isFollowed"},{"location":"sp/docs/social/#stopfollowing","text":"Makes the current user stop following a user, document, site, or tag import { sp , SocialActorType } from \"@pnp/sp\" ; // pass the same social actor struct as shown in follow example for each type const r = await sp . social . stopFollowing ({ AccountName : \"i:0#.f|membership|person@tenant.com\" , ActorType : SocialActorType.User , });","title":"stopFollowing"},{"location":"sp/docs/social/#my","text":"","title":"my"},{"location":"sp/docs/social/#get","text":"Gets this user's social information import { sp } from \"@pnp/sp\" ; const r = await sp . social . my . get ();","title":"get"},{"location":"sp/docs/social/#followed","text":"Gets users, documents, sites, and tags that the current user is following based on the supplied flags. import { sp , SocialActorTypes } from \"@pnp/sp\" ; // get all the followed documents const r1 = await sp . social . my . followed ( SocialActorTypes . Document ); // get all the followed documents and sites const r2 = await sp . social . my . followed ( SocialActorTypes . Document | SocialActorTypes . Site ); // get all the followed sites updated in the last 24 hours const r3 = await sp . social . my . followed ( SocialActorTypes . Site | SocialActorTypes . WithinLast24Hours );","title":"followed"},{"location":"sp/docs/social/#followedcount","text":"Works as followed but returns on the count of actors specifed by the query import { sp , SocialActorTypes } from \"@pnp/sp\" ; // get the followed documents count const r = await sp . social . my . followedCount ( SocialActorTypes . Document );","title":"followedCount"},{"location":"sp/docs/social/#followers","text":"Gets the users who are following the current user. import { sp } from \"@pnp/sp\" ; // get the followed documents count const r = await sp . social . my . followers ();","title":"followers"},{"location":"sp/docs/social/#suggestions","text":"Gets users who the current user might want to follow. import { sp } from \"@pnp/sp\" ; // get the followed documents count const r = await sp . social . my . suggestions ();","title":"suggestions"},{"location":"sp/docs/sp-utilities-utility/","text":"@pnp/sp/utilities \u00b6 Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching. sendEmail \u00b6 This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below). EmailProperties \u00b6 export interface EmailProperties { To : string []; CC? : string []; BCC? : string []; Subject : string ; Body : string ; AdditionalHeaders? : TypedHash < string > ; From? : string ; } Usage \u00b6 You must define the To, Subject, and Body values - the remaining are optional. import { sp , EmailProperties } from \"@pnp/sp\" ; const emailProps : EmailProperties = { To : [ \"user@site.com\" ], CC : [ \"user2@site.com\" , \"user3@site.com\" ], Subject : \"This email is about...\" , Body : \"Here is the body. It supports html\" , }; sp . utility . sendEmail ( emailProps ). then ( _ => { console . log ( \"Email Sent!\" ); }); getCurrentUserEmailAddresses \u00b6 This method returns the current user's email addresses known to SharePoint. import { sp } from \"@pnp/sp\" ; sp . utility . getCurrentUserEmailAddresses (). then (( addressString : string ) => { console . log ( addressString ); }); resolvePrincipal \u00b6 Gets information about a principal that matches the specified Search criteria import { sp , PrincipalType , PrincipalSource , PrincipalInfo } from \"@pnp/sp\" ; sp . utility . resolvePrincipal ( \"user@site.com\" , PrincipalType . User , PrincipalSource . All , true , false ). then (( principal : PrincipalInfo ) => { console . log ( principal ); }); searchPrincipals \u00b6 Gets information about the principals that match the specified Search criteria. import { sp , PrincipalType , PrincipalSource , PrincipalInfo } from \"@pnp/sp\" ; sp . utility . searchPrincipals ( \"john\" , PrincipalType . User , PrincipalSource . All , \"\" , 10 ). then (( principals : PrincipalInfo []) => { console . log ( principals ); }); createEmailBodyForInvitation \u00b6 Gets the external (outside the firewall) URL to a document or resource in a site. import { sp } from \"@pnp/sp\" ; sp . utility . createEmailBodyForInvitation ( \"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\" ). then (( r : string ) => { console . log ( r ); }); expandGroupsToPrincipals \u00b6 Resolves the principals contained within the supplied groups import { sp , PrincipalInfo } from \"@pnp/sp\" ; sp . utility . expandGroupsToPrincipals ([ \"Dev Owners\" , \"Dev Members\" ]). then (( principals : PrincipalInfo []) => { console . log ( principals ); }); // optionally supply a max results count. Default is 30. sp . utility . expandGroupsToPrincipals ([ \"Dev Owners\" , \"Dev Members\" ], 10 ). then (( principals : PrincipalInfo []) => { console . log ( principals ); }); createWikiPage \u00b6 import { sp , CreateWikiPageResult } from \"@pnp/sp\" ; sp . utility . createWikiPage ({ ServerRelativeUrl : \"/sites/dev/SitePages/mynewpage.aspx\" , WikiHtmlContent : \"This is my page content. It supports rich html.\" , }). then (( result : CreateWikiPageResult ) => { // result contains the raw data returned by the service console . log ( result . data ); // result contains a File instance you can use to further update the new page result . file . get (). then ( f => { console . log ( f ); }); }); containsInvalidFileFolderChars \u00b6 Checks if file or folder name contains invalid characters import { sp } from \"@pnp/sp\" ; const isInvalid = sp . utility . containsInvalidFileFolderChars ( \"Filename?.txt\" ); console . log ( isInvalid ); // true stripInvalidFileFolderChars \u00b6 Removes invalid characters from file or folder name import { sp } from \"@pnp/sp\" ; const validName = sp . utility . stripInvalidFileFolderChars ( \"Filename?.txt\" ); console . log ( validName ); // Filename.txt Call Other Methods \u00b6 Even if a method does not have an explicit implementation on the utility api you can still call it using the UtilityMethod class. In this example we will show calling the GetLowerCaseString method, but the technique works for any of the utility methods. import { UtilityMethod } from \"@pnp/sp\" ; // the first parameter is the web url. You can use an empty string for the current web, // or specify it to call other web's. The second parameter is the method name. const method = new UtilityMethod ( \"\" , \"GetLowerCaseString\" ); // you must supply the correctly formatted parameters to the execute method which // is generic and types the result as the supplied generic type parameter. method . excute < string > ({ sourceValue : \"HeRe IS my StrINg\" , lcid : 1033 , }). then (( s : string ) => { console . log ( s ); });","title":"SP.Utilities.Utility"},{"location":"sp/docs/sp-utilities-utility/#pnpsputilities","text":"Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.","title":"@pnp/sp/utilities"},{"location":"sp/docs/sp-utilities-utility/#sendemail","text":"This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).","title":"sendEmail"},{"location":"sp/docs/sp-utilities-utility/#emailproperties","text":"export interface EmailProperties { To : string []; CC? : string []; BCC? : string []; Subject : string ; Body : string ; AdditionalHeaders? : TypedHash < string > ; From? : string ; }","title":"EmailProperties"},{"location":"sp/docs/sp-utilities-utility/#usage","text":"You must define the To, Subject, and Body values - the remaining are optional. import { sp , EmailProperties } from \"@pnp/sp\" ; const emailProps : EmailProperties = { To : [ \"user@site.com\" ], CC : [ \"user2@site.com\" , \"user3@site.com\" ], Subject : \"This email is about...\" , Body : \"Here is the body. It supports html\" , }; sp . utility . sendEmail ( emailProps ). then ( _ => { console . log ( \"Email Sent!\" ); });","title":"Usage"},{"location":"sp/docs/sp-utilities-utility/#getcurrentuseremailaddresses","text":"This method returns the current user's email addresses known to SharePoint. import { sp } from \"@pnp/sp\" ; sp . utility . getCurrentUserEmailAddresses (). then (( addressString : string ) => { console . log ( addressString ); });","title":"getCurrentUserEmailAddresses"},{"location":"sp/docs/sp-utilities-utility/#resolveprincipal","text":"Gets information about a principal that matches the specified Search criteria import { sp , PrincipalType , PrincipalSource , PrincipalInfo } from \"@pnp/sp\" ; sp . utility . resolvePrincipal ( \"user@site.com\" , PrincipalType . User , PrincipalSource . All , true , false ). then (( principal : PrincipalInfo ) => { console . log ( principal ); });","title":"resolvePrincipal"},{"location":"sp/docs/sp-utilities-utility/#searchprincipals","text":"Gets information about the principals that match the specified Search criteria. import { sp , PrincipalType , PrincipalSource , PrincipalInfo } from \"@pnp/sp\" ; sp . utility . searchPrincipals ( \"john\" , PrincipalType . User , PrincipalSource . All , \"\" , 10 ). then (( principals : PrincipalInfo []) => { console . log ( principals ); });","title":"searchPrincipals"},{"location":"sp/docs/sp-utilities-utility/#createemailbodyforinvitation","text":"Gets the external (outside the firewall) URL to a document or resource in a site. import { sp } from \"@pnp/sp\" ; sp . utility . createEmailBodyForInvitation ( \"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\" ). then (( r : string ) => { console . log ( r ); });","title":"createEmailBodyForInvitation"},{"location":"sp/docs/sp-utilities-utility/#expandgroupstoprincipals","text":"Resolves the principals contained within the supplied groups import { sp , PrincipalInfo } from \"@pnp/sp\" ; sp . utility . expandGroupsToPrincipals ([ \"Dev Owners\" , \"Dev Members\" ]). then (( principals : PrincipalInfo []) => { console . log ( principals ); }); // optionally supply a max results count. Default is 30. sp . utility . expandGroupsToPrincipals ([ \"Dev Owners\" , \"Dev Members\" ], 10 ). then (( principals : PrincipalInfo []) => { console . log ( principals ); });","title":"expandGroupsToPrincipals"},{"location":"sp/docs/sp-utilities-utility/#createwikipage","text":"import { sp , CreateWikiPageResult } from \"@pnp/sp\" ; sp . utility . createWikiPage ({ ServerRelativeUrl : \"/sites/dev/SitePages/mynewpage.aspx\" , WikiHtmlContent : \"This is my page content. It supports rich html.\" , }). then (( result : CreateWikiPageResult ) => { // result contains the raw data returned by the service console . log ( result . data ); // result contains a File instance you can use to further update the new page result . file . get (). then ( f => { console . log ( f ); }); });","title":"createWikiPage"},{"location":"sp/docs/sp-utilities-utility/#containsinvalidfilefolderchars","text":"Checks if file or folder name contains invalid characters import { sp } from \"@pnp/sp\" ; const isInvalid = sp . utility . containsInvalidFileFolderChars ( \"Filename?.txt\" ); console . log ( isInvalid ); // true","title":"containsInvalidFileFolderChars"},{"location":"sp/docs/sp-utilities-utility/#stripinvalidfilefolderchars","text":"Removes invalid characters from file or folder name import { sp } from \"@pnp/sp\" ; const validName = sp . utility . stripInvalidFileFolderChars ( \"Filename?.txt\" ); console . log ( validName ); // Filename.txt","title":"stripInvalidFileFolderChars"},{"location":"sp/docs/sp-utilities-utility/#call-other-methods","text":"Even if a method does not have an explicit implementation on the utility api you can still call it using the UtilityMethod class. In this example we will show calling the GetLowerCaseString method, but the technique works for any of the utility methods. import { UtilityMethod } from \"@pnp/sp\" ; // the first parameter is the web url. You can use an empty string for the current web, // or specify it to call other web's. The second parameter is the method name. const method = new UtilityMethod ( \"\" , \"GetLowerCaseString\" ); // you must supply the correctly formatted parameters to the execute method which // is generic and types the result as the supplied generic type parameter. method . excute < string > ({ sourceValue : \"HeRe IS my StrINg\" , lcid : 1033 , }). then (( s : string ) => { console . log ( s ); });","title":"Call Other Methods"},{"location":"sp/docs/tenant-properties/","text":"@pnp/sp/web - tenant properties \u00b6 You can set, read, and remove tenant properties using the methods shown below: setStorageEntity \u00b6 This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp\" ; const w = new Web ( \"https://tenant.sharepoint.com/sites/appcatalog/\" ); // specify required key and value await w . setStorageEntity ( \"Test1\" , \"Value 1\" ); // specify optional description and comments await w . setStorageEntity ( \"Test2\" , \"Value 2\" , \"description\" , \"comments\" ); getStorageEntity \u00b6 This method can be used from any web to retrieve values previsouly set. import { sp , StorageEntity } from \"@pnp/sp\" ; const prop : StorageEntity = await sp . web . getStorageEntity ( \"Test1\" ); console . log ( prop . Value ); removeStorageEntity \u00b6 This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp\" ; const w = new Web ( \"https://tenant.sharepoint.com/sites/appcatalog/\" ); await w . removeStorageEntity ( \"Test1\" );","title":"Tenant Properties"},{"location":"sp/docs/tenant-properties/#pnpspweb-tenant-properties","text":"You can set, read, and remove tenant properties using the methods shown below:","title":"@pnp/sp/web - tenant properties"},{"location":"sp/docs/tenant-properties/#setstorageentity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp\" ; const w = new Web ( \"https://tenant.sharepoint.com/sites/appcatalog/\" ); // specify required key and value await w . setStorageEntity ( \"Test1\" , \"Value 1\" ); // specify optional description and comments await w . setStorageEntity ( \"Test2\" , \"Value 2\" , \"description\" , \"comments\" );","title":"setStorageEntity"},{"location":"sp/docs/tenant-properties/#getstorageentity","text":"This method can be used from any web to retrieve values previsouly set. import { sp , StorageEntity } from \"@pnp/sp\" ; const prop : StorageEntity = await sp . web . getStorageEntity ( \"Test1\" ); console . log ( prop . Value );","title":"getStorageEntity"},{"location":"sp/docs/tenant-properties/#removestorageentity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp\" ; const w = new Web ( \"https://tenant.sharepoint.com/sites/appcatalog/\" ); await w . removeStorageEntity ( \"Test1\" );","title":"removeStorageEntity"},{"location":"sp/docs/views/","text":"@pnp/sp/views \u00b6 Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view. Get a View's Properties \u00b6 To get a views properties you need to know it's id or title. You can use the standard OData operators as expected to select properties. For a list of the properties, please see this article . import { sp } from \"@pnp/sp\" ; // know a view's GUID id sp . web . lists . getByTitle ( \"Documents\" ). getView ( \"2B382C69-DF64-49C4-85F1-70FB9CECACFE\" ). select ( \"Title\" ). get (). then ( v => { console . log ( v ); }); // get by the display title of the view sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"All Documents\" ). select ( \"Title\" ). get (). then ( v => { console . log ( v ); }); Add a View \u00b6 To add a view you use the add method of the views collection. You must supply a title and can supply other parameters as well. import { sp , ViewAddResult } from \"@pnp/sp\" ; // create a new view with default fields and properties sp . web . lists . getByTitle ( \"Documents\" ). views . add ( \"My New View\" ). then ( v => { console . log ( v ); }); // create a new view with specific properties sp . web . lists . getByTitle ( \"Documents\" ). views . add ( \"My New View 2\" , false , { RowLimit : 10 , ViewQuery : \"\" , }). then (( v : ViewAddResult ) => { // manipulate the view's fields v . view . fields . removeAll (). then ( _ => { Promise . all ([ v . view . fields . add ( \"Title\" ), v . view . fields . add ( \"Modified\" ), ]). then ( _ => { console . log ( \"View created\" ); }); }); }); Update a View \u00b6 import { sp , ViewUpdateResult } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"My New View\" ). update ({ RowLimit : 20 , }). then (( v : ViewUpdateResult ) => { console . log ( v ); }); Set View XML \u00b6 Added in 1.2.6 import { sp } from \"@pnp/sp\" ; const viewXml : string = \"...\" ; await sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"My New View\" ). setViewXml ( viewXml ); Delete a View \u00b6 import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"My New View\" ). delete (). then ( _ => { console . log ( \"View deleted\" ); });","title":"Views"},{"location":"sp/docs/views/#pnpspviews","text":"Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.","title":"@pnp/sp/views"},{"location":"sp/docs/views/#get-a-views-properties","text":"To get a views properties you need to know it's id or title. You can use the standard OData operators as expected to select properties. For a list of the properties, please see this article . import { sp } from \"@pnp/sp\" ; // know a view's GUID id sp . web . lists . getByTitle ( \"Documents\" ). getView ( \"2B382C69-DF64-49C4-85F1-70FB9CECACFE\" ). select ( \"Title\" ). get (). then ( v => { console . log ( v ); }); // get by the display title of the view sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"All Documents\" ). select ( \"Title\" ). get (). then ( v => { console . log ( v ); });","title":"Get a View's Properties"},{"location":"sp/docs/views/#add-a-view","text":"To add a view you use the add method of the views collection. You must supply a title and can supply other parameters as well. import { sp , ViewAddResult } from \"@pnp/sp\" ; // create a new view with default fields and properties sp . web . lists . getByTitle ( \"Documents\" ). views . add ( \"My New View\" ). then ( v => { console . log ( v ); }); // create a new view with specific properties sp . web . lists . getByTitle ( \"Documents\" ). views . add ( \"My New View 2\" , false , { RowLimit : 10 , ViewQuery : \"\" , }). then (( v : ViewAddResult ) => { // manipulate the view's fields v . view . fields . removeAll (). then ( _ => { Promise . all ([ v . view . fields . add ( \"Title\" ), v . view . fields . add ( \"Modified\" ), ]). then ( _ => { console . log ( \"View created\" ); }); }); });","title":"Add a View"},{"location":"sp/docs/views/#update-a-view","text":"import { sp , ViewUpdateResult } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"My New View\" ). update ({ RowLimit : 20 , }). then (( v : ViewUpdateResult ) => { console . log ( v ); });","title":"Update a View"},{"location":"sp/docs/views/#set-view-xml","text":"Added in 1.2.6 import { sp } from \"@pnp/sp\" ; const viewXml : string = \"...\" ; await sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"My New View\" ). setViewXml ( viewXml );","title":"Set View XML"},{"location":"sp/docs/views/#delete-a-view","text":"import { sp } from \"@pnp/sp\" ; sp . web . lists . getByTitle ( \"Documents\" ). views . getByTitle ( \"My New View\" ). delete (). then ( _ => { console . log ( \"View deleted\" ); });","title":"Delete a View"},{"location":"sp/docs/webs/","text":"@pnp/sp/webs \u00b6 Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types. Add a Web \u00b6 Using the library you can add a web to another web's collection of subwebs. The basic usage requires only a title and url. This will result in a team site with all of the default settings. import { sp , WebAddResult } from \"@pnp/sp\" ; sp . web . webs . add ( \"title\" , \"subweb1\" ). then (( w : WebAddResult ) => { // show the response from the server when adding the web console . log ( w . data ); w . web . select ( \"Title\" ). get (). then ( w => { // show our title console . log ( w . Title ); }); }); You can also provide other settings such as description, template, language, and inherit permissions. import { sp , WebAddResult } from \"@pnp/sp\" ; // create a German language wiki site with title, url, description, which inherits permissions sp . web . webs . add ( \"wiki\" , \"subweb2\" , \"a wiki web\" , \"WIKI#0\" , 1031 , true ). then (( w : WebAddResult ) => { // show the response from the server when adding the web console . log ( w . data ); w . web . select ( \"Title\" ). get (). then ( w => { // show our title console . log ( w . Title ); }); }); Create Default Associated Groups \u00b6 If you create a web that doesn't inherit permissions from the parent web, you can create its default associated groups (Members, Owners, Visitors) with the default role assigments (Contribute, Full Control, Read) import { sp , WebAddResult } from \"@pnp/sp\" ; sp . web . webs . add ( \"title\" , \"subweb1\" , \"a wiki web\" , \"WIKI#0\" , 1031 , false ). then (( w : WebAddResult ) => { w . web . createDefaultAssociatedGroups (). then (() => { // ... }); }); Get A Web's properties \u00b6 import { sp } from \"@pnp/sp\" ; // basic get of the webs properties sp . web . get (). then ( w => { console . log ( w . Title ); }); // use odata operators to get specific fields sp . web . select ( \"Title\" ). get (). then ( w => { console . log ( w . Title ); }); // use with get to give the result a type sp . web . select ( \"Title\" ). get < { Title : string } > (). then ( w => { console . log ( w . Title ); }); Get Complex Properties \u00b6 Some properties, such as AllProperties, are not returned by default. You can still access them using the expand operator. import { sp } from \"@pnp/sp\" ; sp . web . select ( \"AllProperties\" ). expand ( \"AllProperties\" ). get (). then ( w => { console . log ( w . AllProperties ); }); Get a Web Directly \u00b6 You can also use the Web object directly to get any web, though of course the current user must have the necessary permissions. This is done by importing the web object. import { Web } from \"@pnp/sp\" ; let web = new Web ( \"https://my-tenant.sharepoint.com/sites/mysite\" ); web . get (). then ( w => { console . log ( w ); }); Open Web By Id \u00b6 Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. sp . site . openWebById ( \"111ca453-90f5-482e-a381-cee1ff383c9e\" ). then ( w => { //we got all the data from the web as well console . log ( w . data ); // we can chain w . web . select ( \"Title\" ). get (). then ( w2 => { // ... }); }); Update Web Properties \u00b6 You can update web properties using the update method. The properties available for update are listed in this table . Updating is a simple as passing a plain object with the properties you want to update. import { Web } from \"@pnp/sp\" ; let web = new Web ( \"https://my-tenant.sharepoint.com/sites/mysite\" ); web . update ({ Title : \"New Title\" , CustomMasterUrl : \"{path to masterpage}\" , Description : \"My new description\" , }). then ( w => { console . log ( w ); }); Delete a Web \u00b6 import { Web } from \"@pnp/sp\" ; let web = new Web ( \"https://my-tenant.sharepoint.com/sites/mysite\" ); web . delete (). then ( w => { console . log ( w ); });","title":"Webs"},{"location":"sp/docs/webs/#pnpspwebs","text":"Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.","title":"@pnp/sp/webs"},{"location":"sp/docs/webs/#add-a-web","text":"Using the library you can add a web to another web's collection of subwebs. The basic usage requires only a title and url. This will result in a team site with all of the default settings. import { sp , WebAddResult } from \"@pnp/sp\" ; sp . web . webs . add ( \"title\" , \"subweb1\" ). then (( w : WebAddResult ) => { // show the response from the server when adding the web console . log ( w . data ); w . web . select ( \"Title\" ). get (). then ( w => { // show our title console . log ( w . Title ); }); }); You can also provide other settings such as description, template, language, and inherit permissions. import { sp , WebAddResult } from \"@pnp/sp\" ; // create a German language wiki site with title, url, description, which inherits permissions sp . web . webs . add ( \"wiki\" , \"subweb2\" , \"a wiki web\" , \"WIKI#0\" , 1031 , true ). then (( w : WebAddResult ) => { // show the response from the server when adding the web console . log ( w . data ); w . web . select ( \"Title\" ). get (). then ( w => { // show our title console . log ( w . Title ); }); });","title":"Add a Web"},{"location":"sp/docs/webs/#create-default-associated-groups","text":"If you create a web that doesn't inherit permissions from the parent web, you can create its default associated groups (Members, Owners, Visitors) with the default role assigments (Contribute, Full Control, Read) import { sp , WebAddResult } from \"@pnp/sp\" ; sp . web . webs . add ( \"title\" , \"subweb1\" , \"a wiki web\" , \"WIKI#0\" , 1031 , false ). then (( w : WebAddResult ) => { w . web . createDefaultAssociatedGroups (). then (() => { // ... }); });","title":"Create Default Associated Groups"},{"location":"sp/docs/webs/#get-a-webs-properties","text":"import { sp } from \"@pnp/sp\" ; // basic get of the webs properties sp . web . get (). then ( w => { console . log ( w . Title ); }); // use odata operators to get specific fields sp . web . select ( \"Title\" ). get (). then ( w => { console . log ( w . Title ); }); // use with get to give the result a type sp . web . select ( \"Title\" ). get < { Title : string } > (). then ( w => { console . log ( w . Title ); });","title":"Get A Web's properties"},{"location":"sp/docs/webs/#get-complex-properties","text":"Some properties, such as AllProperties, are not returned by default. You can still access them using the expand operator. import { sp } from \"@pnp/sp\" ; sp . web . select ( \"AllProperties\" ). expand ( \"AllProperties\" ). get (). then ( w => { console . log ( w . AllProperties ); });","title":"Get Complex Properties"},{"location":"sp/docs/webs/#get-a-web-directly","text":"You can also use the Web object directly to get any web, though of course the current user must have the necessary permissions. This is done by importing the web object. import { Web } from \"@pnp/sp\" ; let web = new Web ( \"https://my-tenant.sharepoint.com/sites/mysite\" ); web . get (). then ( w => { console . log ( w ); });","title":"Get a Web Directly"},{"location":"sp/docs/webs/#open-web-by-id","text":"Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. sp . site . openWebById ( \"111ca453-90f5-482e-a381-cee1ff383c9e\" ). then ( w => { //we got all the data from the web as well console . log ( w . data ); // we can chain w . web . select ( \"Title\" ). get (). then ( w2 => { // ... }); });","title":"Open Web By Id"},{"location":"sp/docs/webs/#update-web-properties","text":"You can update web properties using the update method. The properties available for update are listed in this table . Updating is a simple as passing a plain object with the properties you want to update. import { Web } from \"@pnp/sp\" ; let web = new Web ( \"https://my-tenant.sharepoint.com/sites/mysite\" ); web . update ({ Title : \"New Title\" , CustomMasterUrl : \"{path to masterpage}\" , Description : \"My new description\" , }). then ( w => { console . log ( w ); });","title":"Update Web Properties"},{"location":"sp/docs/webs/#delete-a-web","text":"import { Web } from \"@pnp/sp\" ; let web = new Web ( \"https://my-tenant.sharepoint.com/sites/mysite\" ); web . delete (). then ( w => { console . log ( w ); });","title":"Delete a Web"},{"location":"sp-addinhelpers/docs/","text":"@pnp/sp-addinhelpers \u00b6 This module contains classes to allow use of the libraries within a SharePoint add-in. Getting Started \u00b6 Install the library and all dependencies, npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-addinhelpers --save Now you can make requests to the host web from your add-in using the crossDomainWeb method. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp , SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\" ; // this only needs to be done once within your application sp . setup ({ sp : { fetchClientFactory : () => { return new SPRequestExecutorClient (); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\" ; const hostWebUrl = \"{The host web url, likely from the query string}\" ; // make requests into the host web via the SP.RequestExecutor sp . crossDomainWeb ( addInWenUrl , hostWebUrl ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); }); Libary Topics \u00b6 SPRequestExecutorClient SPRestAddIn UML \u00b6 Graphical UML diagram of @pnp/sp-addinhelpers. Right-click the diagram and open in new tab if it is too small.","title":"sp-addinhelpers"},{"location":"sp-addinhelpers/docs/#pnpsp-addinhelpers","text":"This module contains classes to allow use of the libraries within a SharePoint add-in.","title":"@pnp/sp-addinhelpers"},{"location":"sp-addinhelpers/docs/#getting-started","text":"Install the library and all dependencies, npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-addinhelpers --save Now you can make requests to the host web from your add-in using the crossDomainWeb method. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp , SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\" ; // this only needs to be done once within your application sp . setup ({ sp : { fetchClientFactory : () => { return new SPRequestExecutorClient (); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\" ; const hostWebUrl = \"{The host web url, likely from the query string}\" ; // make requests into the host web via the SP.RequestExecutor sp . crossDomainWeb ( addInWenUrl , hostWebUrl ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"Getting Started"},{"location":"sp-addinhelpers/docs/#libary-topics","text":"SPRequestExecutorClient SPRestAddIn","title":"Libary Topics"},{"location":"sp-addinhelpers/docs/#uml","text":"Graphical UML diagram of @pnp/sp-addinhelpers. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"sp-addinhelpers/docs/sp-request-executor-client/","text":"@pnp/sp-addinhelpers/sprequestexecutorclient \u00b6 The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request. Setup \u00b6 To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp , SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\" ; // this only needs to be done once within your application sp . setup ({ sp : { fetchClientFactory : () => { return new SPRequestExecutorClient (); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\" ; const hostWebUrl = \"{The host web url, likely from the query string}\" ; // make requests into the host web via the SP.RequestExecutor sp . crossDomainWeb ( addInWenUrl , hostWebUrl ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"SPRequestExecutorClient"},{"location":"sp-addinhelpers/docs/sp-request-executor-client/#pnpsp-addinhelperssprequestexecutorclient","text":"The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request.","title":"@pnp/sp-addinhelpers/sprequestexecutorclient"},{"location":"sp-addinhelpers/docs/sp-request-executor-client/#setup","text":"To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp , SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\" ; // this only needs to be done once within your application sp . setup ({ sp : { fetchClientFactory : () => { return new SPRequestExecutorClient (); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\" ; const hostWebUrl = \"{The host web url, likely from the query string}\" ; // make requests into the host web via the SP.RequestExecutor sp . crossDomainWeb ( addInWenUrl , hostWebUrl ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"Setup"},{"location":"sp-addinhelpers/docs/sp-rest-addin/","text":"@pnp/sp-addinhelpers/sprestaddin \u00b6 This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp , SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\" ; // this only needs to be done once within your application sp . setup ({ sp : { fetchClientFactory : () => { return new SPRequestExecutorClient (); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\" ; const hostWebUrl = \"{The host web url, likely from the query string}\" ; // make requests into the host web via the SP.RequestExecutor sp . crossDomainWeb ( addInWenUrl , hostWebUrl ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"SPRestAddIn"},{"location":"sp-addinhelpers/docs/sp-rest-addin/#pnpsp-addinhelperssprestaddin","text":"This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp , SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\" ; // this only needs to be done once within your application sp . setup ({ sp : { fetchClientFactory : () => { return new SPRequestExecutorClient (); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\" ; const hostWebUrl = \"{The host web url, likely from the query string}\" ; // make requests into the host web via the SP.RequestExecutor sp . crossDomainWeb ( addInWenUrl , hostWebUrl ). get (). then ( w => { console . log ( JSON . stringify ( w , null , 4 )); });","title":"@pnp/sp-addinhelpers/sprestaddin"},{"location":"sp-addinhelpers/node_modules/@types/microsoft-ajax/","text":"Installation \u00b6 npm install --save @types/microsoft-ajax Summary \u00b6 This package contains type definitions for Microsoft ASP.NET Ajax client side library (http://msdn.microsoft.com/en-us/library/ee341002(v=vs.100).aspx). Details \u00b6 Files were exported from https://www.github.com/DefinitelyTyped/DefinitelyTyped/tree/main/types/microsoft-ajax Additional Details * Last updated: Mon, 21 Aug 2017 21:55:03 GMT * Dependencies: none * Global values: $addHandler, $addHandlers, $clearHandlers, $create, $find, $get, $removeHandler, Sys, Type Credits \u00b6 These definitions were written by Patrick Magee https://github.com/pjmagee .","title":"Installation"},{"location":"sp-addinhelpers/node_modules/@types/microsoft-ajax/#installation","text":"npm install --save @types/microsoft-ajax","title":"Installation"},{"location":"sp-addinhelpers/node_modules/@types/microsoft-ajax/#summary","text":"This package contains type definitions for Microsoft ASP.NET Ajax client side library (http://msdn.microsoft.com/en-us/library/ee341002(v=vs.100).aspx).","title":"Summary"},{"location":"sp-addinhelpers/node_modules/@types/microsoft-ajax/#details","text":"Files were exported from https://www.github.com/DefinitelyTyped/DefinitelyTyped/tree/main/types/microsoft-ajax Additional Details * Last updated: Mon, 21 Aug 2017 21:55:03 GMT * Dependencies: none * Global values: $addHandler, $addHandlers, $clearHandlers, $create, $find, $get, $removeHandler, Sys, Type","title":"Details"},{"location":"sp-addinhelpers/node_modules/@types/microsoft-ajax/#credits","text":"These definitions were written by Patrick Magee https://github.com/pjmagee .","title":"Credits"},{"location":"sp-addinhelpers/node_modules/@types/sharepoint/","text":"Installation \u00b6 npm install --save @types/sharepoint Summary \u00b6 This package contains type definitions for Microsoft SharePoint: (https://msdn.microsoft.com/en-us/library/office/jj193034.aspx). Details \u00b6 Files were exported from https://www.github.com/DefinitelyTyped/DefinitelyTyped/tree/main/types/sharepoint Additional Details * Last updated: Thu, 15 Jun 2017 22:02:56 GMT * Dependencies: microsoft-ajax * Global values: $addRenderContextCallback, $contentLineText, $findResultObjectFromDOM, $getCachedItemValue, $getClientControl, $getItemValue, $getResultItem, $getResultObject, $htmlEncode, $imgSrcUrl, $includeCSS, $includeLanguageScript, $includeScript, $isEmptyArray, $isEmptyString, $isInArray, $isNull, $registerResourceDictionary, $resource, $scriptEncode, $setItemWrapperCallback, $setResultItem, $setResultObject, $urlHtmlEncode, $urlKeyValueEncode, $urlPathEncode, AddEvtHandler, AjaxNavigate, BrowserDetection, BrowserStorage, Browseris, CSSUtil, Callout, CalloutAction, CalloutActionMenu, CalloutActionMenuEntry, CalloutActionOptions, CalloutManager, CalloutOpenOptions, CalloutOptions, CoreRender, DOM, Define, Encoding, ExecuteOrDelayUntilBodyLoaded, ExecuteOrDelayUntilEventNotified, ExecuteOrDelayUntilScriptLoaded, GenerateIID, GenerateIIDForListItem, GetCurrentCtx, GetUrlKeyValue, IE8Support, JSRequest, ListModule, Microsoft, Nav, RefreshCommandUI, RegisterModuleInit, SP, SPAnimation, SPAnimationUtility, SPClientAutoFill, SPClientForms, SPClientPeoplePicker, SPClientPeoplePickerCSRTemplate, SPClientPeoplePickerMRU, SPClientPeoplePickerProcessedUser, SPClientTemplates, SPFieldAttachments_Default, SPFieldBoolean_Edit, SPFieldChoice_Dropdown_Edit, SPFieldChoice_Edit, SPFieldChoice_Radio_Edit, SPFieldDateTime_Display, SPFieldDateTime_Edit, SPFieldFile_Display, SPFieldFile_Edit, SPFieldLookupMulti_Edit, SPFieldLookup_Display, SPFieldLookup_Edit, SPFieldMultiChoice_Edit, SPFieldNote_Display, SPFieldNote_Edit, SPFieldNumber_Edit, SPFieldText_Edit, SPFieldUrl_Display, SPFieldUrl_Edit, SPFieldUserMulti_Display, SPFieldUser_Display, SPField_FormDisplay_Default, SPField_FormDisplay_DefaultNoEncode, SPField_FormDisplay_Empty, SPFormControl_AppendValidationErrorMessage, SPMgr, SPNotifications, SPStatusNotificationData, SPThemeUtils, STSHtmlDecode, STSHtmlEncode, SetFullScreenMode, Srch, StringUtil, Strings, TypeUtil, URI_Encoding, Verify, _spBodyOnLoadCalled, _spBodyOnLoadFunctionNames, _spBodyOnLoadFunctions, _spFriendlyUrlPageContextInfo, _spPageContextInfo, ajaxNavigate, browseris, m$, spMgr Credits \u00b6 These definitions were written by Stanislav Vyshchepan http:// blog.gandjustas.ru , Andrey Markeev http:// markeev.com , Vincent Biret https://github.com/baywet .","title":"Installation"},{"location":"sp-addinhelpers/node_modules/@types/sharepoint/#installation","text":"npm install --save @types/sharepoint","title":"Installation"},{"location":"sp-addinhelpers/node_modules/@types/sharepoint/#summary","text":"This package contains type definitions for Microsoft SharePoint: (https://msdn.microsoft.com/en-us/library/office/jj193034.aspx).","title":"Summary"},{"location":"sp-addinhelpers/node_modules/@types/sharepoint/#details","text":"Files were exported from https://www.github.com/DefinitelyTyped/DefinitelyTyped/tree/main/types/sharepoint Additional Details * Last updated: Thu, 15 Jun 2017 22:02:56 GMT * Dependencies: microsoft-ajax * Global values: $addRenderContextCallback, $contentLineText, $findResultObjectFromDOM, $getCachedItemValue, $getClientControl, $getItemValue, $getResultItem, $getResultObject, $htmlEncode, $imgSrcUrl, $includeCSS, $includeLanguageScript, $includeScript, $isEmptyArray, $isEmptyString, $isInArray, $isNull, $registerResourceDictionary, $resource, $scriptEncode, $setItemWrapperCallback, $setResultItem, $setResultObject, $urlHtmlEncode, $urlKeyValueEncode, $urlPathEncode, AddEvtHandler, AjaxNavigate, BrowserDetection, BrowserStorage, Browseris, CSSUtil, Callout, CalloutAction, CalloutActionMenu, CalloutActionMenuEntry, CalloutActionOptions, CalloutManager, CalloutOpenOptions, CalloutOptions, CoreRender, DOM, Define, Encoding, ExecuteOrDelayUntilBodyLoaded, ExecuteOrDelayUntilEventNotified, ExecuteOrDelayUntilScriptLoaded, GenerateIID, GenerateIIDForListItem, GetCurrentCtx, GetUrlKeyValue, IE8Support, JSRequest, ListModule, Microsoft, Nav, RefreshCommandUI, RegisterModuleInit, SP, SPAnimation, SPAnimationUtility, SPClientAutoFill, SPClientForms, SPClientPeoplePicker, SPClientPeoplePickerCSRTemplate, SPClientPeoplePickerMRU, SPClientPeoplePickerProcessedUser, SPClientTemplates, SPFieldAttachments_Default, SPFieldBoolean_Edit, SPFieldChoice_Dropdown_Edit, SPFieldChoice_Edit, SPFieldChoice_Radio_Edit, SPFieldDateTime_Display, SPFieldDateTime_Edit, SPFieldFile_Display, SPFieldFile_Edit, SPFieldLookupMulti_Edit, SPFieldLookup_Display, SPFieldLookup_Edit, SPFieldMultiChoice_Edit, SPFieldNote_Display, SPFieldNote_Edit, SPFieldNumber_Edit, SPFieldText_Edit, SPFieldUrl_Display, SPFieldUrl_Edit, SPFieldUserMulti_Display, SPFieldUser_Display, SPField_FormDisplay_Default, SPField_FormDisplay_DefaultNoEncode, SPField_FormDisplay_Empty, SPFormControl_AppendValidationErrorMessage, SPMgr, SPNotifications, SPStatusNotificationData, SPThemeUtils, STSHtmlDecode, STSHtmlEncode, SetFullScreenMode, Srch, StringUtil, Strings, TypeUtil, URI_Encoding, Verify, _spBodyOnLoadCalled, _spBodyOnLoadFunctionNames, _spBodyOnLoadFunctions, _spFriendlyUrlPageContextInfo, _spPageContextInfo, ajaxNavigate, browseris, m$, spMgr","title":"Details"},{"location":"sp-addinhelpers/node_modules/@types/sharepoint/#credits","text":"These definitions were written by Stanislav Vyshchepan http:// blog.gandjustas.ru , Andrey Markeev http:// markeev.com , Vincent Biret https://github.com/baywet .","title":"Credits"},{"location":"sp-addinhelpers/node_modules/tslib/","text":"tslib \u00b6 This is a runtime library for TypeScript that contains all of the TypeScript helper functions. This library is primarily used by the --importHelpers flag in TypeScript. When using --importHelpers , a module that uses helper functions like __extends and __assign in the following emitted file: var __assign = ( this && this . __assign ) || Object . assign || function ( t ) { for ( var s , i = 1 , n = arguments . length ; i < n ; i ++ ) { s = arguments [ i ]; for ( var p in s ) if ( Object . prototype . hasOwnProperty . call ( s , p )) t [ p ] = s [ p ]; } return t ; }; exports . x = {}; exports . y = __assign ({}, exports . x ); will instead be emitted as something like the following: var tslib_1 = require ( \"tslib\" ); exports . x = {}; exports . y = tslib_1 . __assign ({}, exports . x ); Because this can avoid duplicate declarations of things like __extends , __assign , etc., this means delivering users smaller files on average, as well as less runtime overhead. For optimized bundles with TypeScript, you should absolutely consider using tslib and --importHelpers . Installing \u00b6 For the latest stable version, run: npm \u00b6 # TypeScript 2.3.3 or later npm install --save tslib # TypeScript 2.3.2 or earlier npm install --save tslib@1.6.1 bower \u00b6 # TypeScript 2.3.3 or later bower install tslib # TypeScript 2.3.2 or earlier bower install tslib@1.6.1 JSPM \u00b6 # TypeScript 2.3.3 or later jspm install tslib # TypeScript 2.3.2 or earlier jspm install tslib@1.6.1 Usage \u00b6 Set the importHelpers compiler option on the command line: tsc --importHelpers file.ts or in your tsconfig.json: { \"compilerOptions\" : { \"importHelpers\" : true } } For bower and JSPM users \u00b6 You will need to add a paths mapping for tslib , e.g. For Bower users: { \"compilerOptions\" : { \"module\" : \"amd\" , \"importHelpers\" : true , \"baseUrl\" : \"./\" , \"paths\" : { \"tslib\" : [ \"bower_components/tslib/tslib.d.ts\" ] } } } For JSPM users: { \"compilerOptions\" : { \"module\" : \"system\" , \"importHelpers\" : true , \"baseUrl\" : \"./\" , \"paths\" : { \"tslib\" : [ \"jspm_packages/npm/tslib@1.9.3/tslib.d.ts\" ] } } } Contribute \u00b6 There are many ways to contribute to TypeScript. Submit bugs and help us verify fixes as they are checked in. Review the source code changes . Engage with other TypeScript users and developers on StackOverflow . Join the #typescript discussion on Twitter. Contribute bug fixes . Read the language specification ( docx , pdf ). Documentation \u00b6 Quick tutorial Programming handbook Language specification Homepage","title":"tslib"},{"location":"sp-addinhelpers/node_modules/tslib/#tslib","text":"This is a runtime library for TypeScript that contains all of the TypeScript helper functions. This library is primarily used by the --importHelpers flag in TypeScript. When using --importHelpers , a module that uses helper functions like __extends and __assign in the following emitted file: var __assign = ( this && this . __assign ) || Object . assign || function ( t ) { for ( var s , i = 1 , n = arguments . length ; i < n ; i ++ ) { s = arguments [ i ]; for ( var p in s ) if ( Object . prototype . hasOwnProperty . call ( s , p )) t [ p ] = s [ p ]; } return t ; }; exports . x = {}; exports . y = __assign ({}, exports . x ); will instead be emitted as something like the following: var tslib_1 = require ( \"tslib\" ); exports . x = {}; exports . y = tslib_1 . __assign ({}, exports . x ); Because this can avoid duplicate declarations of things like __extends , __assign , etc., this means delivering users smaller files on average, as well as less runtime overhead. For optimized bundles with TypeScript, you should absolutely consider using tslib and --importHelpers .","title":"tslib"},{"location":"sp-addinhelpers/node_modules/tslib/#installing","text":"For the latest stable version, run:","title":"Installing"},{"location":"sp-addinhelpers/node_modules/tslib/#npm","text":"# TypeScript 2.3.3 or later npm install --save tslib # TypeScript 2.3.2 or earlier npm install --save tslib@1.6.1","title":"npm"},{"location":"sp-addinhelpers/node_modules/tslib/#bower","text":"# TypeScript 2.3.3 or later bower install tslib # TypeScript 2.3.2 or earlier bower install tslib@1.6.1","title":"bower"},{"location":"sp-addinhelpers/node_modules/tslib/#jspm","text":"# TypeScript 2.3.3 or later jspm install tslib # TypeScript 2.3.2 or earlier jspm install tslib@1.6.1","title":"JSPM"},{"location":"sp-addinhelpers/node_modules/tslib/#usage","text":"Set the importHelpers compiler option on the command line: tsc --importHelpers file.ts or in your tsconfig.json: { \"compilerOptions\" : { \"importHelpers\" : true } }","title":"Usage"},{"location":"sp-addinhelpers/node_modules/tslib/#for-bower-and-jspm-users","text":"You will need to add a paths mapping for tslib , e.g. For Bower users: { \"compilerOptions\" : { \"module\" : \"amd\" , \"importHelpers\" : true , \"baseUrl\" : \"./\" , \"paths\" : { \"tslib\" : [ \"bower_components/tslib/tslib.d.ts\" ] } } } For JSPM users: { \"compilerOptions\" : { \"module\" : \"system\" , \"importHelpers\" : true , \"baseUrl\" : \"./\" , \"paths\" : { \"tslib\" : [ \"jspm_packages/npm/tslib@1.9.3/tslib.d.ts\" ] } } }","title":"For bower and JSPM users"},{"location":"sp-addinhelpers/node_modules/tslib/#contribute","text":"There are many ways to contribute to TypeScript. Submit bugs and help us verify fixes as they are checked in. Review the source code changes . Engage with other TypeScript users and developers on StackOverflow . Join the #typescript discussion on Twitter. Contribute bug fixes . Read the language specification ( docx , pdf ).","title":"Contribute"},{"location":"sp-addinhelpers/node_modules/tslib/#documentation","text":"Quick tutorial Programming handbook Language specification Homepage","title":"Documentation"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/","text":"The __generator helper \u00b6 The __generator helper is a function designed to support TypeScript's down-level emit for async functions when targeting ES5 and earlier. But how, exactly, does it work? Here's the body of the __generator helper: __generator = function ( thisArg , body ) { var _ = { label : 0 , sent : function () { if ( t [ 0 ] & 1 ) throw t [ 1 ]; return t [ 1 ]; }, trys : [], ops : [] }, f , y , t ; return { next : verb ( 0 ), \"throw\" : verb ( 1 ), \"return\" : verb ( 2 ) }; function verb ( n ) { return function ( v ) { return step ([ n , v ]); }; } function step ( op ) { if ( f ) throw new TypeError ( \"Generator is already executing.\" ); while ( _ ) try { if ( f = 1 , y && ( t = y [ op [ 0 ] & 2 ? \"return\" : op [ 0 ] ? \"throw\" : \"next\" ]) && ! ( t = t . call ( y , op [ 1 ])). done ) return t ; if ( y = 0 , t ) op = [ 0 , t . value ]; switch ( op [ 0 ]) { case 0 : case 1 : t = op ; break ; case 4 : _ . label ++ ; return { value : op [ 1 ], done : false }; case 5 : _ . label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } op = body . call ( thisArg , _ ); } catch ( e ) { op = [ 6 , e ]; y = 0 ; } finally { f = t = 0 ; } if ( op [ 0 ] & 5 ) throw op [ 1 ]; return { value : op [ 0 ] ? op [ 1 ] : void 0 , done : true }; } }; And here's an example of it in use: // source async function func ( x ) { try { await x ; } catch ( e ) { console . error ( e ); } finally { console . log ( \"finally\" ); } } // generated function func ( x ) { return __awaiter ( this , void 0 , void 0 , function () { var e_1 ; return __generator ( this , function ( _a ) { switch ( _a . label ) { case 0 : _a.trys.push ([ 0 , 1 , 3 , 4 ]); return [ 4 /*yield*/ , x ]; case 1 : _a.sent (); return [ 3 /*break*/ , 4 ]; case 2 : e_1 = _a . sent (); console . error ( e_1 ); return [ 3 /*break*/ , 4 ]; case 3 : console.log ( \"finally\" ); return [ 7 /*endfinally*/ ]; case 4 : return [ 2 /*return*/ ]; } }); }); } There is a lot going on in this function, so the following will break down what each part of the __generator helper does and how it works. Opcodes \u00b6 The __generator helper uses opcodes which represent various operations that are interpreted by the helper to affect its internal state. The following table lists the various opcodes, their arguments, and their purpose: Opcode Arguments Purpose 0 (next) value Starts the generator, or resumes the generator with value as the result of the AwaitExpression where execution was paused. 1 (throw) value Resumes the generator, throwing value at AwaitExpression where execution was paused. 2 (return) value Exits the generator, executing any finally blocks starting at the AwaitExpression where execution was paused. 3 (break) label Performs an unconditional jump to the specified label, executing any finally between the current instruction and the label. 4 (yield) value Suspends the generator, setting the resume point at the next label and yielding the value. 5 (yieldstar) value Suspends the generator, setting the resume point at the next label and delegating operations to the supplied value. 6 (catch) error An internal instruction used to indicate an exception that was thrown from the body of the generator. 7 (endfinally) Exits a finally block, resuming any previous operation (such as a break, return, throw, etc.) State \u00b6 The _ , f , y , and t variables make up the persistent state of the __generator function. Each variable has a specific purpose, as described in the following sections: The _ variable \u00b6 The __generator helper must share state between its internal step orchestration function and the body function passed to the helper. var _ = { label : 0 , sent : function () { if ( t [ 0 ] & 1 ) // NOTE: true for `throw`, but not `next` or `catch` throw t [ 1 ]; return sent [ 1 ]; }, trys : [], ops : [] }; The following table describes the members of the _ state object and their purpose: Name Description label Specifies the next switch case to execute in the body function. sent Handles the completion result passed to the generator. trys A stack of Protected Regions , which are 4-tuples that describe the labels that make up a try..catch..finally block. ops A stack of pending operations used for try..finally blocks. The __generator helper passes this state object to the body function for use with switching between switch cases in the body, handling completions from AwaitExpression , etc. The f variable \u00b6 The f variable indicates whether the generator is currently executing, to prevent re-entry of the same generator during its execution. The y variable \u00b6 The y variable stores the iterator passed to a yieldstar instruction to which operations should be delegated. The t variable \u00b6 The t variable is a temporary variable that stores one of the following values: The completion value when resuming from a yield or yield* . The error value for a catch block. The current Protected Region . The verb ( next , throw , or return method) to delegate to the expression of a yield* . The result of evaluating the verb delegated to the expression of a yield* . NOTE: None of the above cases overlap. Protected Regions \u00b6 A Protected Region is a region within the body function that indicates a try..catch..finally statement. It consists of a 4-tuple that contains 4 labels: Offset Description 0 Required The label that indicates the beginning of a try..catch..finally statement. 1 Optional The label that indicates the beginning of a catch clause. 2 Optional The label that indicates the beginning of a finally clause. 3 Required The label that indicates the end of the try..catch..finally statement. The generator object \u00b6 The final step of the __generator helper is the allocation of an object that implements the Generator protocol, to be used by the __awaiter helper: return { next : verb ( 0 ), \"throw\" : verb ( 1 ), \"return\" : verb ( 2 ) }; function verb ( n ) { return function ( v ) { return step ([ n , v ]); }; } This object translates calls to next , throw , and return to the appropriate Opcodes and invokes the step orchestration function to continue execution. The throw and return method names are quoted to better support ES3. Orchestration \u00b6 The step function is the main orechestration mechanism for the __generator helper. It interprets opcodes, handles protected regions , and communicates results back to the caller. Here's a closer look at the step function: function step ( op ) { if ( f ) throw new TypeError ( \"Generator is already executing.\" ); while ( _ ) try { if ( f = 1 , y && ( t = y [ op [ 0 ] & 2 ? \"return\" : op [ 0 ] ? \"throw\" : \"next\" ]) && ! ( t = t . call ( y , op [ 1 ])). done ) return t ; if ( y = 0 , t ) op = [ 0 , t . value ]; switch ( op [ 0 ]) { case 0 : case 1 : t = op ; break ; case 4 : _.label ++ ; return { value : op [ 1 ], done : false }; case 5 : _.label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } op = body . call ( thisArg , _ ); } catch ( e ) { op = [ 6 , e ]; y = 0 ; } finally { f = t = 0 ; } if ( op [ 0 ] & 5 ) throw op [ 1 ]; return { value : op [ 0 ] ? op [ 1 ] : void 0 , done : true }; } The main body of step exists in a while loop. This allows us to continually interpret operations until we have reached some completion value, be it a return , await , or throw . Preventing re-entry \u00b6 The first part of the step function is used as a check to prevent re-entry into a currently executing generator: if ( f ) throw new TypeError ( \"Generator is already executing.\" ); Running the generator \u00b6 The main body of the step function consists of a while loop which continues to evaluate instructions until the generator exits or is suspended: while ( _ ) try ... When the generator has run to completion, the _ state variable will be cleared, forcing the loop to exit. Evaluating the generator body. \u00b6 try { ... op = body . call ( thisArg , _ ); } catch ( e ) { op = [ 6 , e ]; y = 0 ; } finally { f = t = 0 ; } Depending on the current operation, we re-enter the generator body to start or continue execution. Here we invoke body with thisArg as the this binding and the _ state object as the only argument. The result is a tuple that contains the next Opcode and argument. If evaluation of the body resulted in an exception, we convert this into an Opcode 6 (\"catch\") operation to be handled in the next spin of the while loop. We also clear the y variable in case it is set to ensure we are no longer delegating operations as the exception occurred in user code outside of, or at the function boundary of, the delegated iterator (otherwise the iterator would have handled the exception itself). After executing user code, we clear the f flag that indicates we are executing the generator, as well as the t temporary value so that we don't hold onto values sent to the generator for longer than necessary. Inside of the try..finally statement are a series of statements that are used to evaluate the operations of the transformed generator body. The first thing we do is mark the generator as executing: if ( f = 1 , ...) Despite the fact this expression is part of the head of an if statement, the comma operator causes it to be evaluated and the result thrown out. This is a minification added purely to reduce the overall footprint of the helper. Delegating yield* \u00b6 The first two statements of the try..finally statement handle delegation for yield* : if ( f = 1 , y && ( t = y [ op [ 0 ] & 2 ? \"return\" : op [ 0 ] ? \"throw\" : \"next\" ]) && ! ( t = t . call ( y , op [ 1 ])). done ) return t ; if ( y = 0 , t ) op = [ 0 , t . value ]; If the y variable is set, and y has a next , throw , or return method (depending on the current operation), we invoke this method and store the return value (an IteratorResult) in t . If t indicates it is a yielded value (e.g. t.done === false ), we return t to the caller. If t indicates it is a returned value (e.g. t.done === true ), we mark the operation with the next Opcode, and the returned value. If y did not have the appropriate method, or t was a returned value, we reset y to a falsey value and continue processing the operation. Handling operations \u00b6 The various Opcodes are handled in the following switch statement: switch ( op [ 0 ]) { case 0 : case 1 : t = op ; break ; case 4 : _.label ++ ; return { value : op [ 1 ], done : false }; case 5 : _.label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } The following sections describe the various Opcodes: Opcode 0 (\"next\") and Opcode 1 (\"throw\") \u00b6 case 0 : // next case 1 : // throw t = op ; break ; Both Opcode 0 (\"next\") and Opcode 1 (\"throw\") have the same behavior. The current operation is stored in the t variable and the body function is invoked. The body function should call _.sent() which will evaluate the appropriate completion result. Opcode 4 (\"yield\") \u00b6 case 4 : // yield _ . label ++ ; return { value : op [ 1 ], done : false }; When we encounter Opcode 4 (\"yield\"), we increment the label by one to indicate the point at which the generator will resume execution. We then return an IteratorResult whose value is the yielded value, and done is false . Opcode 5 (\"yieldstar\") \u00b6 case 5 : // yieldstar _ . label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; When we receive Opcode 5 (\"yieldstar\"), we increment the label by one to indicate the point at which the generator will resume execution. We then store the iterator in op[1] in the y variable, and set the operation to delegate to Opcode 0 (\"next\") with no value. Finally, we continue execution at the top of the loop to start delegation. Opcode 7 (\"endfinally\") \u00b6 case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; Opcode 7 (\"endfinally\") indicates that we have hit the end of a finally clause, and that the last operation recorded before entering the finally block should be evaluated. Opcode 2 (\"return\"), Opcode 3 (\"break\"), and Opcode 6 (\"catch\") \u00b6 default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } The handling for Opcode 2 (\"return\"), Opcode 3 (\"break\") and Opcode 6 (\"catch\") is more complicated, as we must obey the specified runtime semantics of generators. The first line in this clause gets the current Protected Region if found and stores it in the t temp variable: if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ...) ... The remainder of this statement, as well as the following by several if statements test for more complex conditions. The first of these is the following: if ( ! ( t = ...) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } If we encounter an Opcode 6 (\"catch\") or Opcode 2 (\"return\"), and we are not in a protected region, then this operation completes the generator by setting the _ variable to a falsey value. The continue statement resumes execution at the top of the while statement, which will exit the loop so that we continue execution at the statement following the loop. if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } The if statement above handles Opcode 3 (\"break\") when we are either not in a protected region , or are performing an unconditional jump to a label inside of the current protected region . In this case we can unconditionally jump to the specified label. if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } The if statement above handles Opcode 6 (\"catch\") when inside the try block of a protected region . In this case we jump to the catch block, if present. We replace the value of t with the operation so that the exception can be read as the first statement of the transformed catch clause of the transformed generator body. if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } This if statement handles all Opcodes when in a protected region with a finally clause. As long as we are not already inside the finally clause, we jump to the finally clause and push the pending operation onto the _.ops stack. This allows us to resume execution of the pending operation once we have completed execution of the finally clause, as long as it does not supersede this operation with its own completion value. if ( t [ 2 ]) _ . ops . pop (); Any other completion value inside of a finally clause will supersede the pending completion value from the try or catch clauses. The above if statement pops the pending completion from the stack. _ . trys . pop (); continue ; The remaining statements handle the point at which we exit a protected region . Here we pop the current protected region from the stack and spin the while statement to evaluate the current operation again in the next protected region or at the function boundary. Handling a completed generator \u00b6 Once the generator has completed, the _ state variable will be falsey. As a result, the while loop will terminate and hand control off to the final statement of the orchestration function, which deals with how a completed generator is evaluated: if ( op [ 0 ] & 5 ) throw op [ 1 ]; return { value : op [ 0 ] ? op [ 1 ] : void 0 , done : true }; If the caller calls throw on the generator it will send Opcode 1 (\"throw\"). If an exception is uncaught within the body of the generator, it will send Opcode 6 (\"catch\"). As the generator has completed, it throws the exception. Both of these cases are caught by the bitmask 5 , which does not collide with the only two other valid completion Opcodes. If the caller calls next on the generator, it will send Opcode 0 (\"next\"). As the generator has completed, it returns an IteratorResult where value is undefined and done is true. If the caller calls return on the generator, it will send Opcode 2 (\"return\"). As the generator has completed, it returns an IteratorResult where value is the value provided to return , and done is true.","title":"The `__generator` helper"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#the-__generator-helper","text":"The __generator helper is a function designed to support TypeScript's down-level emit for async functions when targeting ES5 and earlier. But how, exactly, does it work? Here's the body of the __generator helper: __generator = function ( thisArg , body ) { var _ = { label : 0 , sent : function () { if ( t [ 0 ] & 1 ) throw t [ 1 ]; return t [ 1 ]; }, trys : [], ops : [] }, f , y , t ; return { next : verb ( 0 ), \"throw\" : verb ( 1 ), \"return\" : verb ( 2 ) }; function verb ( n ) { return function ( v ) { return step ([ n , v ]); }; } function step ( op ) { if ( f ) throw new TypeError ( \"Generator is already executing.\" ); while ( _ ) try { if ( f = 1 , y && ( t = y [ op [ 0 ] & 2 ? \"return\" : op [ 0 ] ? \"throw\" : \"next\" ]) && ! ( t = t . call ( y , op [ 1 ])). done ) return t ; if ( y = 0 , t ) op = [ 0 , t . value ]; switch ( op [ 0 ]) { case 0 : case 1 : t = op ; break ; case 4 : _ . label ++ ; return { value : op [ 1 ], done : false }; case 5 : _ . label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } op = body . call ( thisArg , _ ); } catch ( e ) { op = [ 6 , e ]; y = 0 ; } finally { f = t = 0 ; } if ( op [ 0 ] & 5 ) throw op [ 1 ]; return { value : op [ 0 ] ? op [ 1 ] : void 0 , done : true }; } }; And here's an example of it in use: // source async function func ( x ) { try { await x ; } catch ( e ) { console . error ( e ); } finally { console . log ( \"finally\" ); } } // generated function func ( x ) { return __awaiter ( this , void 0 , void 0 , function () { var e_1 ; return __generator ( this , function ( _a ) { switch ( _a . label ) { case 0 : _a.trys.push ([ 0 , 1 , 3 , 4 ]); return [ 4 /*yield*/ , x ]; case 1 : _a.sent (); return [ 3 /*break*/ , 4 ]; case 2 : e_1 = _a . sent (); console . error ( e_1 ); return [ 3 /*break*/ , 4 ]; case 3 : console.log ( \"finally\" ); return [ 7 /*endfinally*/ ]; case 4 : return [ 2 /*return*/ ]; } }); }); } There is a lot going on in this function, so the following will break down what each part of the __generator helper does and how it works.","title":"The __generator helper"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#opcodes","text":"The __generator helper uses opcodes which represent various operations that are interpreted by the helper to affect its internal state. The following table lists the various opcodes, their arguments, and their purpose: Opcode Arguments Purpose 0 (next) value Starts the generator, or resumes the generator with value as the result of the AwaitExpression where execution was paused. 1 (throw) value Resumes the generator, throwing value at AwaitExpression where execution was paused. 2 (return) value Exits the generator, executing any finally blocks starting at the AwaitExpression where execution was paused. 3 (break) label Performs an unconditional jump to the specified label, executing any finally between the current instruction and the label. 4 (yield) value Suspends the generator, setting the resume point at the next label and yielding the value. 5 (yieldstar) value Suspends the generator, setting the resume point at the next label and delegating operations to the supplied value. 6 (catch) error An internal instruction used to indicate an exception that was thrown from the body of the generator. 7 (endfinally) Exits a finally block, resuming any previous operation (such as a break, return, throw, etc.)","title":"Opcodes"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#state","text":"The _ , f , y , and t variables make up the persistent state of the __generator function. Each variable has a specific purpose, as described in the following sections:","title":"State"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#the-_-variable","text":"The __generator helper must share state between its internal step orchestration function and the body function passed to the helper. var _ = { label : 0 , sent : function () { if ( t [ 0 ] & 1 ) // NOTE: true for `throw`, but not `next` or `catch` throw t [ 1 ]; return sent [ 1 ]; }, trys : [], ops : [] }; The following table describes the members of the _ state object and their purpose: Name Description label Specifies the next switch case to execute in the body function. sent Handles the completion result passed to the generator. trys A stack of Protected Regions , which are 4-tuples that describe the labels that make up a try..catch..finally block. ops A stack of pending operations used for try..finally blocks. The __generator helper passes this state object to the body function for use with switching between switch cases in the body, handling completions from AwaitExpression , etc.","title":"The _ variable"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#the-f-variable","text":"The f variable indicates whether the generator is currently executing, to prevent re-entry of the same generator during its execution.","title":"The f variable"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#the-y-variable","text":"The y variable stores the iterator passed to a yieldstar instruction to which operations should be delegated.","title":"The y variable"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#the-t-variable","text":"The t variable is a temporary variable that stores one of the following values: The completion value when resuming from a yield or yield* . The error value for a catch block. The current Protected Region . The verb ( next , throw , or return method) to delegate to the expression of a yield* . The result of evaluating the verb delegated to the expression of a yield* . NOTE: None of the above cases overlap.","title":"The t variable"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#protected-regions","text":"A Protected Region is a region within the body function that indicates a try..catch..finally statement. It consists of a 4-tuple that contains 4 labels: Offset Description 0 Required The label that indicates the beginning of a try..catch..finally statement. 1 Optional The label that indicates the beginning of a catch clause. 2 Optional The label that indicates the beginning of a finally clause. 3 Required The label that indicates the end of the try..catch..finally statement.","title":"Protected Regions"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#the-generator-object","text":"The final step of the __generator helper is the allocation of an object that implements the Generator protocol, to be used by the __awaiter helper: return { next : verb ( 0 ), \"throw\" : verb ( 1 ), \"return\" : verb ( 2 ) }; function verb ( n ) { return function ( v ) { return step ([ n , v ]); }; } This object translates calls to next , throw , and return to the appropriate Opcodes and invokes the step orchestration function to continue execution. The throw and return method names are quoted to better support ES3.","title":"The generator object"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#orchestration","text":"The step function is the main orechestration mechanism for the __generator helper. It interprets opcodes, handles protected regions , and communicates results back to the caller. Here's a closer look at the step function: function step ( op ) { if ( f ) throw new TypeError ( \"Generator is already executing.\" ); while ( _ ) try { if ( f = 1 , y && ( t = y [ op [ 0 ] & 2 ? \"return\" : op [ 0 ] ? \"throw\" : \"next\" ]) && ! ( t = t . call ( y , op [ 1 ])). done ) return t ; if ( y = 0 , t ) op = [ 0 , t . value ]; switch ( op [ 0 ]) { case 0 : case 1 : t = op ; break ; case 4 : _.label ++ ; return { value : op [ 1 ], done : false }; case 5 : _.label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } op = body . call ( thisArg , _ ); } catch ( e ) { op = [ 6 , e ]; y = 0 ; } finally { f = t = 0 ; } if ( op [ 0 ] & 5 ) throw op [ 1 ]; return { value : op [ 0 ] ? op [ 1 ] : void 0 , done : true }; } The main body of step exists in a while loop. This allows us to continually interpret operations until we have reached some completion value, be it a return , await , or throw .","title":"Orchestration"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#preventing-re-entry","text":"The first part of the step function is used as a check to prevent re-entry into a currently executing generator: if ( f ) throw new TypeError ( \"Generator is already executing.\" );","title":"Preventing re-entry"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#running-the-generator","text":"The main body of the step function consists of a while loop which continues to evaluate instructions until the generator exits or is suspended: while ( _ ) try ... When the generator has run to completion, the _ state variable will be cleared, forcing the loop to exit.","title":"Running the generator"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#evaluating-the-generator-body","text":"try { ... op = body . call ( thisArg , _ ); } catch ( e ) { op = [ 6 , e ]; y = 0 ; } finally { f = t = 0 ; } Depending on the current operation, we re-enter the generator body to start or continue execution. Here we invoke body with thisArg as the this binding and the _ state object as the only argument. The result is a tuple that contains the next Opcode and argument. If evaluation of the body resulted in an exception, we convert this into an Opcode 6 (\"catch\") operation to be handled in the next spin of the while loop. We also clear the y variable in case it is set to ensure we are no longer delegating operations as the exception occurred in user code outside of, or at the function boundary of, the delegated iterator (otherwise the iterator would have handled the exception itself). After executing user code, we clear the f flag that indicates we are executing the generator, as well as the t temporary value so that we don't hold onto values sent to the generator for longer than necessary. Inside of the try..finally statement are a series of statements that are used to evaluate the operations of the transformed generator body. The first thing we do is mark the generator as executing: if ( f = 1 , ...) Despite the fact this expression is part of the head of an if statement, the comma operator causes it to be evaluated and the result thrown out. This is a minification added purely to reduce the overall footprint of the helper.","title":"Evaluating the generator body."},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#delegating-yield","text":"The first two statements of the try..finally statement handle delegation for yield* : if ( f = 1 , y && ( t = y [ op [ 0 ] & 2 ? \"return\" : op [ 0 ] ? \"throw\" : \"next\" ]) && ! ( t = t . call ( y , op [ 1 ])). done ) return t ; if ( y = 0 , t ) op = [ 0 , t . value ]; If the y variable is set, and y has a next , throw , or return method (depending on the current operation), we invoke this method and store the return value (an IteratorResult) in t . If t indicates it is a yielded value (e.g. t.done === false ), we return t to the caller. If t indicates it is a returned value (e.g. t.done === true ), we mark the operation with the next Opcode, and the returned value. If y did not have the appropriate method, or t was a returned value, we reset y to a falsey value and continue processing the operation.","title":"Delegating yield*"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#handling-operations","text":"The various Opcodes are handled in the following switch statement: switch ( op [ 0 ]) { case 0 : case 1 : t = op ; break ; case 4 : _.label ++ ; return { value : op [ 1 ], done : false }; case 5 : _.label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } The following sections describe the various Opcodes:","title":"Handling operations"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#opcode-0-next-and-opcode-1-throw","text":"case 0 : // next case 1 : // throw t = op ; break ; Both Opcode 0 (\"next\") and Opcode 1 (\"throw\") have the same behavior. The current operation is stored in the t variable and the body function is invoked. The body function should call _.sent() which will evaluate the appropriate completion result.","title":"Opcode 0 (\"next\") and Opcode 1 (\"throw\")"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#opcode-4-yield","text":"case 4 : // yield _ . label ++ ; return { value : op [ 1 ], done : false }; When we encounter Opcode 4 (\"yield\"), we increment the label by one to indicate the point at which the generator will resume execution. We then return an IteratorResult whose value is the yielded value, and done is false .","title":"Opcode 4 (\"yield\")"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#opcode-5-yieldstar","text":"case 5 : // yieldstar _ . label ++ ; y = op [ 1 ]; op = [ 0 ]; continue ; When we receive Opcode 5 (\"yieldstar\"), we increment the label by one to indicate the point at which the generator will resume execution. We then store the iterator in op[1] in the y variable, and set the operation to delegate to Opcode 0 (\"next\") with no value. Finally, we continue execution at the top of the loop to start delegation.","title":"Opcode 5 (\"yieldstar\")"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#opcode-7-endfinally","text":"case 7 : op = _ . ops . pop (); _ . trys . pop (); continue ; Opcode 7 (\"endfinally\") indicates that we have hit the end of a finally clause, and that the last operation recorded before entering the finally block should be evaluated.","title":"Opcode 7 (\"endfinally\")"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#opcode-2-return-opcode-3-break-and-opcode-6-catch","text":"default : if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } if ( t [ 2 ]) _ . ops . pop (); _ . trys . pop (); continue ; } The handling for Opcode 2 (\"return\"), Opcode 3 (\"break\") and Opcode 6 (\"catch\") is more complicated, as we must obey the specified runtime semantics of generators. The first line in this clause gets the current Protected Region if found and stores it in the t temp variable: if ( ! ( t = _ . trys , t = t . length > 0 && t [ t . length - 1 ]) && ...) ... The remainder of this statement, as well as the following by several if statements test for more complex conditions. The first of these is the following: if ( ! ( t = ...) && ( op [ 0 ] === 6 || op [ 0 ] === 2 )) { _ = 0 ; continue ; } If we encounter an Opcode 6 (\"catch\") or Opcode 2 (\"return\"), and we are not in a protected region, then this operation completes the generator by setting the _ variable to a falsey value. The continue statement resumes execution at the top of the while statement, which will exit the loop so that we continue execution at the statement following the loop. if ( op [ 0 ] === 3 && ( ! t || ( op [ 1 ] > t [ 0 ] && op [ 1 ] < t [ 3 ]))) { _ . label = op [ 1 ]; break ; } The if statement above handles Opcode 3 (\"break\") when we are either not in a protected region , or are performing an unconditional jump to a label inside of the current protected region . In this case we can unconditionally jump to the specified label. if ( op [ 0 ] === 6 && _ . label < t [ 1 ]) { _ . label = t [ 1 ]; t = op ; break ; } The if statement above handles Opcode 6 (\"catch\") when inside the try block of a protected region . In this case we jump to the catch block, if present. We replace the value of t with the operation so that the exception can be read as the first statement of the transformed catch clause of the transformed generator body. if ( t && _ . label < t [ 2 ]) { _ . label = t [ 2 ]; _ . ops . push ( op ); break ; } This if statement handles all Opcodes when in a protected region with a finally clause. As long as we are not already inside the finally clause, we jump to the finally clause and push the pending operation onto the _.ops stack. This allows us to resume execution of the pending operation once we have completed execution of the finally clause, as long as it does not supersede this operation with its own completion value. if ( t [ 2 ]) _ . ops . pop (); Any other completion value inside of a finally clause will supersede the pending completion value from the try or catch clauses. The above if statement pops the pending completion from the stack. _ . trys . pop (); continue ; The remaining statements handle the point at which we exit a protected region . Here we pop the current protected region from the stack and spin the while statement to evaluate the current operation again in the next protected region or at the function boundary.","title":"Opcode 2 (\"return\"), Opcode 3 (\"break\"), and Opcode 6 (\"catch\")"},{"location":"sp-addinhelpers/node_modules/tslib/docs/generator/#handling-a-completed-generator","text":"Once the generator has completed, the _ state variable will be falsey. As a result, the while loop will terminate and hand control off to the final statement of the orchestration function, which deals with how a completed generator is evaluated: if ( op [ 0 ] & 5 ) throw op [ 1 ]; return { value : op [ 0 ] ? op [ 1 ] : void 0 , done : true }; If the caller calls throw on the generator it will send Opcode 1 (\"throw\"). If an exception is uncaught within the body of the generator, it will send Opcode 6 (\"catch\"). As the generator has completed, it throws the exception. Both of these cases are caught by the bitmask 5 , which does not collide with the only two other valid completion Opcodes. If the caller calls next on the generator, it will send Opcode 0 (\"next\"). As the generator has completed, it returns an IteratorResult where value is undefined and done is true. If the caller calls return on the generator, it will send Opcode 2 (\"return\"). As the generator has completed, it returns an IteratorResult where value is the value provided to return , and done is true.","title":"Handling a completed generator"},{"location":"sp-clientsvc/docs/","text":"@pnp/sp-clientsvc \u00b6 This library provides base classes for working with the legacy SharePoint client.svc/ProcessQuery endpoint. The base classes support most of the possibilities for types of query calls, as well as supporting fluent batching and caching. They are based on the same @pnp/queryable foundation as the other libraries so should feel familiar when extending. You can see @pnp/sp-taxonomy for an example showing how to extend these base classes into a functional fluent model. UML \u00b6 Graphical UML diagram of @pnp/sp-clientsvc. Right-click the diagram and open in new tab if it is too small.","title":"sp-clientsvc"},{"location":"sp-clientsvc/docs/#pnpsp-clientsvc","text":"This library provides base classes for working with the legacy SharePoint client.svc/ProcessQuery endpoint. The base classes support most of the possibilities for types of query calls, as well as supporting fluent batching and caching. They are based on the same @pnp/queryable foundation as the other libraries so should feel familiar when extending. You can see @pnp/sp-taxonomy for an example showing how to extend these base classes into a functional fluent model.","title":"@pnp/sp-clientsvc"},{"location":"sp-clientsvc/docs/#uml","text":"Graphical UML diagram of @pnp/sp-clientsvc. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"sp-taxonomy/docs/","text":"@pnp/sp-taxonomy \u00b6 This module provides a fluent interface for working with the SharePoint term store. It does not rely on SP.taxonomy.js or other dependencies outside the @pnp scope. It is designed to function in a similar manner and present a similar feel to the other data retrieval libraries. It works by calling the \"/_vti_bin/client.svc/ProcessQuery\" endpoint. Getting Started \u00b6 You will need to install the @pnp/sp-taxonomy package as well as the packages it requires to run. npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-taxonomy @pnp/sp-clientsvc --save Root Object \u00b6 All fluent taxonomy operations originate from the Taxonomy object. You can access it in several ways. Import existing instance \u00b6 This method will grab an existing instance of the Taxonomy class and allow you to immediately chain additional methods. import { taxonomy } from \"@pnp/sp-taxonomy\" ; await taxonomy . termStores . get (); Import class and create instance \u00b6 You can also import the Taxonomy class and create a new instance. This useful in those cases where you want to work with taxonomy in another web than the current web. import { Session } from \"@pnp/sp-taxonomy\" ; const taxonomy = new Session ( \"https://mytenant.sharepoint.com/sites/dev\" ); await taxonomy . termStores . get (); Setup \u00b6 Because the sp-taxonomy library uses the same @pnp/queryable request pipeline as the other libraries you can call the setup method with the same options used for the @pnp/sp library. The setup method is provided as shorthand and avoids the need to import anything from @pnp/sp if you do not need to. A call to this setup method is equivilent to calling the sp.setup method and the configuration is shared between the libraries within your application. In the below example all requests for the @pnp/sp-taxonomy library and the @pnp/sp library will be routed through the specified SPFetchClient. Sharing the configuration like this handles the most common scenario of working on the same web easily. You can set other values here as well such as baseUrl and they will be respected by both libraries. import { taxonomy } from \"@pnp/sp-taxonomy\" ; import { SPFetchClient } from \"@pnp/nodejs\" ; // example for setting up the node client using setup method // we also set a custom header, as an example taxonomy . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{url}\" , \"{client id}\" , \"{client secret}\" ); }, headers : { \"X-Custom-Header\" : \"A Great Value\" , }, }, }); Library Topics \u00b6 Term Stores Term Groups Term Sets Terms Labels UML \u00b6 Graphical UML diagram of @pnp/sp-taxonomy. Right-click the diagram and open in new tab if it is too small.","title":"sp-taxonomy"},{"location":"sp-taxonomy/docs/#pnpsp-taxonomy","text":"This module provides a fluent interface for working with the SharePoint term store. It does not rely on SP.taxonomy.js or other dependencies outside the @pnp scope. It is designed to function in a similar manner and present a similar feel to the other data retrieval libraries. It works by calling the \"/_vti_bin/client.svc/ProcessQuery\" endpoint.","title":"@pnp/sp-taxonomy"},{"location":"sp-taxonomy/docs/#getting-started","text":"You will need to install the @pnp/sp-taxonomy package as well as the packages it requires to run. npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-taxonomy @pnp/sp-clientsvc --save","title":"Getting Started"},{"location":"sp-taxonomy/docs/#root-object","text":"All fluent taxonomy operations originate from the Taxonomy object. You can access it in several ways.","title":"Root Object"},{"location":"sp-taxonomy/docs/#import-existing-instance","text":"This method will grab an existing instance of the Taxonomy class and allow you to immediately chain additional methods. import { taxonomy } from \"@pnp/sp-taxonomy\" ; await taxonomy . termStores . get ();","title":"Import existing instance"},{"location":"sp-taxonomy/docs/#import-class-and-create-instance","text":"You can also import the Taxonomy class and create a new instance. This useful in those cases where you want to work with taxonomy in another web than the current web. import { Session } from \"@pnp/sp-taxonomy\" ; const taxonomy = new Session ( \"https://mytenant.sharepoint.com/sites/dev\" ); await taxonomy . termStores . get ();","title":"Import class and create instance"},{"location":"sp-taxonomy/docs/#setup","text":"Because the sp-taxonomy library uses the same @pnp/queryable request pipeline as the other libraries you can call the setup method with the same options used for the @pnp/sp library. The setup method is provided as shorthand and avoids the need to import anything from @pnp/sp if you do not need to. A call to this setup method is equivilent to calling the sp.setup method and the configuration is shared between the libraries within your application. In the below example all requests for the @pnp/sp-taxonomy library and the @pnp/sp library will be routed through the specified SPFetchClient. Sharing the configuration like this handles the most common scenario of working on the same web easily. You can set other values here as well such as baseUrl and they will be respected by both libraries. import { taxonomy } from \"@pnp/sp-taxonomy\" ; import { SPFetchClient } from \"@pnp/nodejs\" ; // example for setting up the node client using setup method // we also set a custom header, as an example taxonomy . setup ({ sp : { fetchClientFactory : () => { return new SPFetchClient ( \"{url}\" , \"{client id}\" , \"{client secret}\" ); }, headers : { \"X-Custom-Header\" : \"A Great Value\" , }, }, });","title":"Setup"},{"location":"sp-taxonomy/docs/#library-topics","text":"Term Stores Term Groups Term Sets Terms Labels","title":"Library Topics"},{"location":"sp-taxonomy/docs/#uml","text":"Graphical UML diagram of @pnp/sp-taxonomy. Right-click the diagram and open in new tab if it is too small.","title":"UML"},{"location":"sp-taxonomy/docs/labels/","text":"@pnp/sp-taxonomy/labels \u00b6 Load labels \u00b6 You can load labels by accessing the labels property of a term . import { ILabel , ILabelData , ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < see terms article for loading term > // load the terms merged with data const labelsWithData : ( ILabel & ILabelData )[] = await term . labels . get (); // get a label by value const label : ILabel = term . labels . getByValue ( \"term value\" ); // get a label merged with data const label2 : ILabel & ILabelData = term . labels . getByValue ( \"term value\" ). get (); Label Properties and Methods \u00b6 setAsDefaultForLanguage \u00b6 Sets this labels as the default for the language import { ILabel , ILabelData , ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < see terms article for loading term > // get a label by value await term . labels . getByValue ( \"term value\" ). setAsDefaultForLanguage (); delete \u00b6 Deletes this label import { ILabel , ILabelData , ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < see terms article for loading term > // get a label by value await term . labels . getByValue ( \"term value\" ). delete ();","title":"Labels"},{"location":"sp-taxonomy/docs/labels/#pnpsp-taxonomylabels","text":"","title":"@pnp/sp-taxonomy/labels"},{"location":"sp-taxonomy/docs/labels/#load-labels","text":"You can load labels by accessing the labels property of a term . import { ILabel , ILabelData , ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < see terms article for loading term > // load the terms merged with data const labelsWithData : ( ILabel & ILabelData )[] = await term . labels . get (); // get a label by value const label : ILabel = term . labels . getByValue ( \"term value\" ); // get a label merged with data const label2 : ILabel & ILabelData = term . labels . getByValue ( \"term value\" ). get ();","title":"Load labels"},{"location":"sp-taxonomy/docs/labels/#label-properties-and-methods","text":"","title":"Label Properties and Methods"},{"location":"sp-taxonomy/docs/labels/#setasdefaultforlanguage","text":"Sets this labels as the default for the language import { ILabel , ILabelData , ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < see terms article for loading term > // get a label by value await term . labels . getByValue ( \"term value\" ). setAsDefaultForLanguage ();","title":"setAsDefaultForLanguage"},{"location":"sp-taxonomy/docs/labels/#delete","text":"Deletes this label import { ILabel , ILabelData , ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < see terms article for loading term > // get a label by value await term . labels . getByValue ( \"term value\" ). delete ();","title":"delete"},{"location":"sp-taxonomy/docs/term-groups/","text":"@pnp/sp-taxonomy/termgroups \u00b6 Term groups are used as a container for terms within a term store. Load a term group \u00b6 Term groups are loaded from a term store import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); Term Group methods and properties \u00b6 addContributor \u00b6 Adds a contributor to the Group import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await group . addContributor ( \"i:0#.f|membership|person@tenant.com\" ); addGroupManager \u00b6 Adds a group manager to the Group import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await group . addGroupManager ( \"i:0#.f|membership|person@tenant.com\" ); createTermSet \u00b6 Creates a new term set import { taxonomy , ITermStore , ITermGroup , ITermSet , ITermSetData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const set : ITermSet & ITermSetData = await group . createTermSet ( \"name\" , 1031 ); // you can optionally supply the term set id, if you do not we create a new id for you const set2 : ITermSet & ITermSetData = await group . createTermSet ( \"name\" , 1031 , \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); get \u00b6 Gets this term group's data import { taxonomy , ITermStore , ITermGroupData , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup & ITermGroupData = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). get ();","title":"Term Groups"},{"location":"sp-taxonomy/docs/term-groups/#pnpsp-taxonomytermgroups","text":"Term groups are used as a container for terms within a term store.","title":"@pnp/sp-taxonomy/termgroups"},{"location":"sp-taxonomy/docs/term-groups/#load-a-term-group","text":"Term groups are loaded from a term store import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"Load a term group"},{"location":"sp-taxonomy/docs/term-groups/#term-group-methods-and-properties","text":"","title":"Term Group methods and properties"},{"location":"sp-taxonomy/docs/term-groups/#addcontributor","text":"Adds a contributor to the Group import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await group . addContributor ( \"i:0#.f|membership|person@tenant.com\" );","title":"addContributor"},{"location":"sp-taxonomy/docs/term-groups/#addgroupmanager","text":"Adds a group manager to the Group import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await group . addGroupManager ( \"i:0#.f|membership|person@tenant.com\" );","title":"addGroupManager"},{"location":"sp-taxonomy/docs/term-groups/#createtermset","text":"Creates a new term set import { taxonomy , ITermStore , ITermGroup , ITermSet , ITermSetData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const set : ITermSet & ITermSetData = await group . createTermSet ( \"name\" , 1031 ); // you can optionally supply the term set id, if you do not we create a new id for you const set2 : ITermSet & ITermSetData = await group . createTermSet ( \"name\" , 1031 , \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"createTermSet"},{"location":"sp-taxonomy/docs/term-groups/#get","text":"Gets this term group's data import { taxonomy , ITermStore , ITermGroupData , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup & ITermGroupData = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). get ();","title":"get"},{"location":"sp-taxonomy/docs/term-sets/","text":"@pnp/sp-taxonomy/termsets \u00b6 Term sets contain terms within the taxonomy heirarchy. Load a term set \u00b6 You load a term set directly from a term store. import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); Or you can load a term set from a collection - though if you know the id it is more efficient to get the term set directly. import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set = store . getTermSetsByName ( \"my set\" , 1031 ). getById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const setWithData = store . getTermSetsByName ( \"my set\" , 1031 ). getByName ( \"my set\" ). get (); Term set methods and properties \u00b6 addStakeholder \u00b6 Adds a stakeholder to the TermSet import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await set . addStakeholder ( \"i:0#.f|membership|person@tenant.com\" ); deleteStakeholder \u00b6 Deletes a stakeholder to the TermSet import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await set . deleteStakeholder ( \"i:0#.f|membership|person@tenant.com\" ); get \u00b6 Gets the data for this TermSet import { taxonomy , ITermStore , ITermSet , ITermSetData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const setWithData : ITermSet & ITermSetData = await set . get (); terms \u00b6 Provides access to the terms collection for this termset import { taxonomy , ITermStore , ITermSet , ITerms , ITermData , ITerm } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const terms : ITerms = set . terms ; // load the data into the terms instances const termsWithData : ( ITermData & ITerm )[] = set . terms . get (); getTermById \u00b6 Gets a term by id from this set import { taxonomy , ITermStore , ITermSet , ITermData , ITerm } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const term : ITerm = set . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); // load the data into the term instances const termWithData : ITermData & ITerm = term . get (); addTerm \u00b6 Adds a term to a term set import { taxonomy , ITermStore , ITermSet , ITermData , ITerm } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const term : ITerm & ITermData = await set . addTerm ( \"name\" , 1031 , true ); // you can optionally set the id when you create the term const term2 : ITerm & ITermData = await set . addTerm ( \"name\" , 1031 , true , \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"Term Sets"},{"location":"sp-taxonomy/docs/term-sets/#pnpsp-taxonomytermsets","text":"Term sets contain terms within the taxonomy heirarchy.","title":"@pnp/sp-taxonomy/termsets"},{"location":"sp-taxonomy/docs/term-sets/#load-a-term-set","text":"You load a term set directly from a term store. import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); Or you can load a term set from a collection - though if you know the id it is more efficient to get the term set directly. import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set = store . getTermSetsByName ( \"my set\" , 1031 ). getById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const setWithData = store . getTermSetsByName ( \"my set\" , 1031 ). getByName ( \"my set\" ). get ();","title":"Load a term set"},{"location":"sp-taxonomy/docs/term-sets/#term-set-methods-and-properties","text":"","title":"Term set methods and properties"},{"location":"sp-taxonomy/docs/term-sets/#addstakeholder","text":"Adds a stakeholder to the TermSet import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await set . addStakeholder ( \"i:0#.f|membership|person@tenant.com\" );","title":"addStakeholder"},{"location":"sp-taxonomy/docs/term-sets/#deletestakeholder","text":"Deletes a stakeholder to the TermSet import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); await set . deleteStakeholder ( \"i:0#.f|membership|person@tenant.com\" );","title":"deleteStakeholder"},{"location":"sp-taxonomy/docs/term-sets/#get","text":"Gets the data for this TermSet import { taxonomy , ITermStore , ITermSet , ITermSetData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const setWithData : ITermSet & ITermSetData = await set . get ();","title":"get"},{"location":"sp-taxonomy/docs/term-sets/#terms","text":"Provides access to the terms collection for this termset import { taxonomy , ITermStore , ITermSet , ITerms , ITermData , ITerm } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const terms : ITerms = set . terms ; // load the data into the terms instances const termsWithData : ( ITermData & ITerm )[] = set . terms . get ();","title":"terms"},{"location":"sp-taxonomy/docs/term-sets/#gettermbyid","text":"Gets a term by id from this set import { taxonomy , ITermStore , ITermSet , ITermData , ITerm } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const term : ITerm = set . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); // load the data into the term instances const termWithData : ITermData & ITerm = term . get ();","title":"getTermById"},{"location":"sp-taxonomy/docs/term-sets/#addterm","text":"Adds a term to a term set import { taxonomy , ITermStore , ITermSet , ITermData , ITerm } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const term : ITerm & ITermData = await set . addTerm ( \"name\" , 1031 , true ); // you can optionally set the id when you create the term const term2 : ITerm & ITermData = await set . addTerm ( \"name\" , 1031 , true , \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"addTerm"},{"location":"sp-taxonomy/docs/term-stores/","text":"@pnp/sp-taxonomy/termstores \u00b6 Term stores contain term groups, term sets, and terms. This article describes how to work find, load, and use a term store to access the terms inside. List term stores \u00b6 You can access a list of all term stores via the termstores property of the Taxonomy class. // get a list of term stores and return all properties const stores = await taxonomy . termStores . get (); // you can also select the fields to return for the term stores using the select operator. const stores2 = await taxonomy . termStores . select ( \"Name\" ). get (); Load a term store \u00b6 To load a specific term store you can use the getByName or getById methods. Using the get method executes the request to the server. const store = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ). get (); const store2 = await taxonomy . termStores . getById ( \"f6112509-fba7-4544-b2ed-ce6c9396b646\" ). get (); // you can use select as well with either method to choose the fields to return const store3 = await taxonomy . termStores . getById ( \"f6112509-fba7-4544-b2ed-ce6c9396b646\" ). select ( \"Name\" ). get (); For term stores and all other objects data is returned as a merger of the data and a new instance of the representative class. Allowing you to immediately begin acting on the object. IF you do not need the data, skip the get call until you do. // no data loaded yet, store is an instance of TermStore class const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); // I can call subsequent methods on the same object and will now have an object with data // I could have called get above as well - this is just an example const store2 : ITermStore & ITermStoreData = await store . get (); // log the Name property console . log ( store2 . Name ); // call another TermStore method on the same object await store2 . addLanguage ( 1031 ); Term store methods and properties \u00b6 get \u00b6 Loads the data for this term store import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ). get (); getTermSetsByName \u00b6 Gets the collection of term sets with a matching name import { taxonomy , ITermSets } from \"@pnp/sp-taxonomy\" ; const sets : ITermSets = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ). getTermSetsByName ( \"My Set\" , 1033 ); getTermSetById \u00b6 Gets the term set with a matching id import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; // note that you can also use instances if you wanted to conduct multiple operations on a single store const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); // we will handle normalizing guids for you as well :) const set2 : ITermSet = store . getTermSetById ( \"{a63aefc9-359d-42b7-a0d2-cb1809acd260}\" ); getTermById \u00b6 Gets a term by id import { taxonomy , ITermStore , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const term : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const termWithData : ITerm & ITermData = await term . get (); getTermsById \u00b6 Added in 1.2.6 import { taxonomy , ITermStore , ITerms , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const terms : ITerms = store . getTermsById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" , \"0ba6845c-1468-4ec5-a5a8-718f1fb05432\" ); const termWithData : ( ITerm & ITermData )[] = await term . get (); getTermGroupById \u00b6 Gets a term group by id import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); getTerms \u00b6 Gets terms that match the provided criteria. Please see this article for details on valid querys. import { taxonomy , ITermStore , ILabelMatchInfo , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const terms : ITerms = store . getTerms ({ TermLabel : \"test label\" , TrimUnavailable : true , }); // load the data based on the above query const termsWithData : ( ITerm & ITermData )[] = terms . get (); // select works here too :) const termsWithData2 : ( ITerm & ITermData )[] = terms . select ( \"Name\" ). get (); addLanguage \u00b6 Adds a language to the term store by LCID import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . addLanguage ( 1031 ); addGroup \u00b6 Adds a term group to the term store import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup & ITermGroupData = await store . addGroup ( \"My Group Name\" ); // you can optionally specify the guid of the group, if you don't we just create a new guid for you const groups : ITermGroup & ITermGroupData = await store . addGroup ( \"My Group Name\" , \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); commitAll \u00b6 Commits all updates to the database that have occurred since the last commit or rollback. import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . commitAll (); deleteLanguage \u00b6 Delete a working language from the TermStore import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . deleteLanguage ( 1031 ); rollbackAll \u00b6 Discards all updates that have occurred since the last commit or rollback. It is unlikely you will need to call this method through this library due to how things are structured. import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . rollbackAll ();","title":"Term Stores"},{"location":"sp-taxonomy/docs/term-stores/#pnpsp-taxonomytermstores","text":"Term stores contain term groups, term sets, and terms. This article describes how to work find, load, and use a term store to access the terms inside.","title":"@pnp/sp-taxonomy/termstores"},{"location":"sp-taxonomy/docs/term-stores/#list-term-stores","text":"You can access a list of all term stores via the termstores property of the Taxonomy class. // get a list of term stores and return all properties const stores = await taxonomy . termStores . get (); // you can also select the fields to return for the term stores using the select operator. const stores2 = await taxonomy . termStores . select ( \"Name\" ). get ();","title":"List term stores"},{"location":"sp-taxonomy/docs/term-stores/#load-a-term-store","text":"To load a specific term store you can use the getByName or getById methods. Using the get method executes the request to the server. const store = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ). get (); const store2 = await taxonomy . termStores . getById ( \"f6112509-fba7-4544-b2ed-ce6c9396b646\" ). get (); // you can use select as well with either method to choose the fields to return const store3 = await taxonomy . termStores . getById ( \"f6112509-fba7-4544-b2ed-ce6c9396b646\" ). select ( \"Name\" ). get (); For term stores and all other objects data is returned as a merger of the data and a new instance of the representative class. Allowing you to immediately begin acting on the object. IF you do not need the data, skip the get call until you do. // no data loaded yet, store is an instance of TermStore class const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); // I can call subsequent methods on the same object and will now have an object with data // I could have called get above as well - this is just an example const store2 : ITermStore & ITermStoreData = await store . get (); // log the Name property console . log ( store2 . Name ); // call another TermStore method on the same object await store2 . addLanguage ( 1031 );","title":"Load a term store"},{"location":"sp-taxonomy/docs/term-stores/#term-store-methods-and-properties","text":"","title":"Term store methods and properties"},{"location":"sp-taxonomy/docs/term-stores/#get","text":"Loads the data for this term store import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ). get ();","title":"get"},{"location":"sp-taxonomy/docs/term-stores/#gettermsetsbyname","text":"Gets the collection of term sets with a matching name import { taxonomy , ITermSets } from \"@pnp/sp-taxonomy\" ; const sets : ITermSets = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ). getTermSetsByName ( \"My Set\" , 1033 );","title":"getTermSetsByName"},{"location":"sp-taxonomy/docs/term-stores/#gettermsetbyid","text":"Gets the term set with a matching id import { taxonomy , ITermStore , ITermSet } from \"@pnp/sp-taxonomy\" ; // note that you can also use instances if you wanted to conduct multiple operations on a single store const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const set : ITermSet = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); // we will handle normalizing guids for you as well :) const set2 : ITermSet = store . getTermSetById ( \"{a63aefc9-359d-42b7-a0d2-cb1809acd260}\" );","title":"getTermSetById"},{"location":"sp-taxonomy/docs/term-stores/#gettermbyid","text":"Gets a term by id import { taxonomy , ITermStore , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const term : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); const termWithData : ITerm & ITermData = await term . get ();","title":"getTermById"},{"location":"sp-taxonomy/docs/term-stores/#gettermsbyid","text":"Added in 1.2.6 import { taxonomy , ITermStore , ITerms , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const terms : ITerms = store . getTermsById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" , \"0ba6845c-1468-4ec5-a5a8-718f1fb05432\" ); const termWithData : ( ITerm & ITermData )[] = await term . get ();","title":"getTermsById"},{"location":"sp-taxonomy/docs/term-stores/#gettermgroupbyid","text":"Gets a term group by id import { taxonomy , ITermStore , ITermGroup } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup = store . getTermGroupById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"getTermGroupById"},{"location":"sp-taxonomy/docs/term-stores/#getterms","text":"Gets terms that match the provided criteria. Please see this article for details on valid querys. import { taxonomy , ITermStore , ILabelMatchInfo , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const terms : ITerms = store . getTerms ({ TermLabel : \"test label\" , TrimUnavailable : true , }); // load the data based on the above query const termsWithData : ( ITerm & ITermData )[] = terms . get (); // select works here too :) const termsWithData2 : ( ITerm & ITermData )[] = terms . select ( \"Name\" ). get ();","title":"getTerms"},{"location":"sp-taxonomy/docs/term-stores/#addlanguage","text":"Adds a language to the term store by LCID import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . addLanguage ( 1031 );","title":"addLanguage"},{"location":"sp-taxonomy/docs/term-stores/#addgroup","text":"Adds a term group to the term store import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const group : ITermGroup & ITermGroupData = await store . addGroup ( \"My Group Name\" ); // you can optionally specify the guid of the group, if you don't we just create a new guid for you const groups : ITermGroup & ITermGroupData = await store . addGroup ( \"My Group Name\" , \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"addGroup"},{"location":"sp-taxonomy/docs/term-stores/#commitall","text":"Commits all updates to the database that have occurred since the last commit or rollback. import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . commitAll ();","title":"commitAll"},{"location":"sp-taxonomy/docs/term-stores/#deletelanguage","text":"Delete a working language from the TermStore import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . deleteLanguage ( 1031 );","title":"deleteLanguage"},{"location":"sp-taxonomy/docs/term-stores/#rollbackall","text":"Discards all updates that have occurred since the last commit or rollback. It is unlikely you will need to call this method through this library due to how things are structured. import { taxonomy , ITermStore } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); await store . rollbackAll ();","title":"rollbackAll"},{"location":"sp-taxonomy/docs/terms/","text":"@pnp/sp-taxonomy/terms \u00b6 Terms are the individual entries with a term set. Load Terms \u00b6 You can load a collection of terms through a term set or term store . import { taxonomy , ITermStore , ITerms , ILabelMatchInfo , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const labelMatchInfo : ILabelMatchInfo = { TermLabel : \"My Label\" , TrimUnavailable : true , }; const terms : ITerms = store . getTerms ( labelMatchInfo ); // get term instances merged with data const terms2 : ( ITermData & ITerm )[] = await store . getTerms ( labelMatchInfo ). get (); const terms3 : ITerms = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). terms ; // get terms merged with data from a term set const terms4 : ( ITerm & ITermData )[] = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). terms . get (); Load Single Term \u00b6 You can get a single term a variety of ways as shown below. The \"best\" way will be determined by what information is available to do the lookup but ultimately will result in the same end product. import { taxonomy , ITermStore , ITerms , ILabelMatchInfo , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); // get a single term by id const term : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); // get single get merged with data const term2 : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). get (); // use select to choose which fields to return const term3 : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). select ( \"Name\" ). get (); // get a term from a term set const term4 : ITerm = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); Term methods and properties \u00b6 labels \u00b6 Accesses the labels collection for this term import { taxonomy , ITermStore , ITerm , ILabels } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; const labels : ILabels = term . labels ; // labels merged with data const labelsWithData = term . labels . get (); createLabel \u00b6 Creates a new label for this Term import { taxonomy , ITermStore , ITerm , ILabelData , ILabel } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; const label : ILabelData & ILabel = term . createLabel ( \"label text\" , 1031 ); // optionally specify this is the default label const label2 : ILabelData & ILabel = term . createLabel ( \"label text\" , 1031 , true ); deprecate \u00b6 Sets the deprecation flag on a term import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; await term . deprecate ( true ); get \u00b6 Loads the term data import { ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data const term2 : ITerm & ITermData = await term . get (); getDescription \u00b6 Sets the description import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data const description = await term . getDescription ( 1031 ); setDescription \u00b6 Sets the description import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data await term . setDescription ( \"the description\" , 1031 ); setLocalCustomProperty \u00b6 Sets a custom property on this term import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data await term . setLocalCustomProperty ( \"name\" , \"value\" ); addTerm \u00b6 Added in 1.2.8 Adds a child term to an existing term instance. import { ITerm } from \"@pnp/sp-taxonomy\" ; const parentTerm : ITerm = < from one of the above methods > ; await parentTerm . addTerm ( \"child 1\" , 1033 ); await parentTerm . addTerm ( \"child 2\" , 1033 );","title":"Terms"},{"location":"sp-taxonomy/docs/terms/#pnpsp-taxonomyterms","text":"Terms are the individual entries with a term set.","title":"@pnp/sp-taxonomy/terms"},{"location":"sp-taxonomy/docs/terms/#load-terms","text":"You can load a collection of terms through a term set or term store . import { taxonomy , ITermStore , ITerms , ILabelMatchInfo , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); const labelMatchInfo : ILabelMatchInfo = { TermLabel : \"My Label\" , TrimUnavailable : true , }; const terms : ITerms = store . getTerms ( labelMatchInfo ); // get term instances merged with data const terms2 : ( ITermData & ITerm )[] = await store . getTerms ( labelMatchInfo ). get (); const terms3 : ITerms = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). terms ; // get terms merged with data from a term set const terms4 : ( ITerm & ITermData )[] = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). terms . get ();","title":"Load Terms"},{"location":"sp-taxonomy/docs/terms/#load-single-term","text":"You can get a single term a variety of ways as shown below. The \"best\" way will be determined by what information is available to do the lookup but ultimately will result in the same end product. import { taxonomy , ITermStore , ITerms , ILabelMatchInfo , ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const store : ITermStore = await taxonomy . termStores . getByName ( \"Taxonomy_v5o/SbcTE2cegwO2dtAN9l==\" ); // get a single term by id const term : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ); // get single get merged with data const term2 : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). get (); // use select to choose which fields to return const term3 : ITerm = store . getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). select ( \"Name\" ). get (); // get a term from a term set const term4 : ITerm = store . getTermSetById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" ). getTermById ( \"0ba6845c-1468-4ec5-a5a8-718f1fb05431\" );","title":"Load Single Term"},{"location":"sp-taxonomy/docs/terms/#term-methods-and-properties","text":"","title":"Term methods and properties"},{"location":"sp-taxonomy/docs/terms/#labels","text":"Accesses the labels collection for this term import { taxonomy , ITermStore , ITerm , ILabels } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; const labels : ILabels = term . labels ; // labels merged with data const labelsWithData = term . labels . get ();","title":"labels"},{"location":"sp-taxonomy/docs/terms/#createlabel","text":"Creates a new label for this Term import { taxonomy , ITermStore , ITerm , ILabelData , ILabel } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; const label : ILabelData & ILabel = term . createLabel ( \"label text\" , 1031 ); // optionally specify this is the default label const label2 : ILabelData & ILabel = term . createLabel ( \"label text\" , 1031 , true );","title":"createLabel"},{"location":"sp-taxonomy/docs/terms/#deprecate","text":"Sets the deprecation flag on a term import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; await term . deprecate ( true );","title":"deprecate"},{"location":"sp-taxonomy/docs/terms/#get","text":"Loads the term data import { ITerm , ITermData } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data const term2 : ITerm & ITermData = await term . get ();","title":"get"},{"location":"sp-taxonomy/docs/terms/#getdescription","text":"Sets the description import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data const description = await term . getDescription ( 1031 );","title":"getDescription"},{"location":"sp-taxonomy/docs/terms/#setdescription","text":"Sets the description import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data await term . setDescription ( \"the description\" , 1031 );","title":"setDescription"},{"location":"sp-taxonomy/docs/terms/#setlocalcustomproperty","text":"Sets a custom property on this term import { ITerm } from \"@pnp/sp-taxonomy\" ; const term : ITerm = < from one of the above methods > ; // load term instance merged with data await term . setLocalCustomProperty ( \"name\" , \"value\" );","title":"setLocalCustomProperty"},{"location":"sp-taxonomy/docs/terms/#addterm","text":"Added in 1.2.8 Adds a child term to an existing term instance. import { ITerm } from \"@pnp/sp-taxonomy\" ; const parentTerm : ITerm = < from one of the above methods > ; await parentTerm . addTerm ( \"child 1\" , 1033 ); await parentTerm . addTerm ( \"child 2\" , 1033 );","title":"addTerm"},{"location":"sp-taxonomy/docs/utilities/","text":"@pnp/sp-taxonomy/utilities \u00b6 These are a collection of helper methods you may find useful. setItemMetaDataField \u00b6 Allows you to easily set the value of a metadata field in a list item. import { sp } from \"@pnp/sp\" ; import { taxonomy , setItemMetaDataField } from \"@pnp/sp-taxonomy\" ; // create a new item, or load an existing const itemResult = await sp . web . lists . getByTitle ( \"TaxonomyList\" ). items . add ({ Title : \"My Title\" , }); // get a term const term = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221111\" ). get (); setItemMetaDataField ( itemResult . item , \"MetaDataFieldName\" , term ); setItemMetaDataMultiField \u00b6 Allows you to easily set the value of a multi-value metadata field in a list item. import { sp } from \"@pnp/sp\" ; import { taxonomy , setItemMetaDataMultiField } from \"@pnp/sp-taxonomy\" ; // create a new item, or load an existing const itemResult = await sp . web . lists . getByTitle ( \"TaxonomyList\" ). items . add ({ Title : \"My Title\" , }); // get a term const term = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221111\" ). get (); // get another term const term2 = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221112\" ). get (); // get yet another term const term3 = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221113\" ). get (); setItemMetaDataMultiField ( itemResult . item , \"MultiValueMetaDataFieldName\" , term , term2 , term3 );","title":"Utilities"},{"location":"sp-taxonomy/docs/utilities/#pnpsp-taxonomyutilities","text":"These are a collection of helper methods you may find useful.","title":"@pnp/sp-taxonomy/utilities"},{"location":"sp-taxonomy/docs/utilities/#setitemmetadatafield","text":"Allows you to easily set the value of a metadata field in a list item. import { sp } from \"@pnp/sp\" ; import { taxonomy , setItemMetaDataField } from \"@pnp/sp-taxonomy\" ; // create a new item, or load an existing const itemResult = await sp . web . lists . getByTitle ( \"TaxonomyList\" ). items . add ({ Title : \"My Title\" , }); // get a term const term = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221111\" ). get (); setItemMetaDataField ( itemResult . item , \"MetaDataFieldName\" , term );","title":"setItemMetaDataField"},{"location":"sp-taxonomy/docs/utilities/#setitemmetadatamultifield","text":"Allows you to easily set the value of a multi-value metadata field in a list item. import { sp } from \"@pnp/sp\" ; import { taxonomy , setItemMetaDataMultiField } from \"@pnp/sp-taxonomy\" ; // create a new item, or load an existing const itemResult = await sp . web . lists . getByTitle ( \"TaxonomyList\" ). items . add ({ Title : \"My Title\" , }); // get a term const term = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221111\" ). get (); // get another term const term2 = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221112\" ). get (); // get yet another term const term3 = await taxonomy . getDefaultSiteCollectionTermStore () . getTermById ( \"99992696-1111-1111-1111-15e65b221113\" ). get (); setItemMetaDataMultiField ( itemResult . item , \"MultiValueMetaDataFieldName\" , term , term2 , term3 );","title":"setItemMetaDataMultiField"}]} \ No newline at end of file diff --git a/v1/sitemap.xml b/v1/sitemap.xml new file mode 100644 index 000000000..8d198947b --- /dev/null +++ b/v1/sitemap.xml @@ -0,0 +1,428 @@ + + + + https://pnp.github.io/pnpjs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/getting-started/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/transition-guide/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/getting-started/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/getting-started-dev/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/gulp-commands/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/debugging/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/documentation/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/deployment/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/beta-versions/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/polyfill/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/package-structure/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/SPFx-On-Premesis-2016/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/transition-guide/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/documentation/packages/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/adalclient/ + 2019-12-20 + daily + + + + + daily + + + https://pnp.github.io/pnpjs/common/docs/collections/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/custom-httpclientimpl/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/libconfig/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/netutil/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/storage/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/common/docs/util/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/config-store/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/config-store/docs/configuration/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/config-store/docs/providers/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/contacts/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/directoryobjects/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/invitations/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/onedrive/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/planner/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/subscriptions/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/graph/docs/teams/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/logging/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/nodejs/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/nodejs/docs/adal-fetch-client/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/nodejs/docs/sp-fetch-client/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/nodejs/docs/bearer-token-fetch-client/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/nodejs/docs/provider-hosted-app/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/caching/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/core/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/odata-batch/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/parsers/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/pipeline/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/queryable/docs/queryable/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/pnpjs/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/alias-parameters/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/alm/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/attachments/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/client-side-pages/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/content-types/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/entity-merging/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/features/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/fields/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/files/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/items/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/navigation-service/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/permissions/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/profiles/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/related-items/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/search/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/sharing/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/sites/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/sitedesigns/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/social/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/sp-utilities-utility/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/tenant-properties/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/views/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/webs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp/docs/comments-likes/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-addinhelpers/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-addinhelpers/docs/sp-request-executor-client/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-addinhelpers/docs/sp-rest-addin/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-clientsvc/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/term-stores/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/term-groups/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/term-sets/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/terms/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/labels/ + 2019-12-20 + daily + + + https://pnp.github.io/pnpjs/sp-taxonomy/docs/utilities/ + 2019-12-20 + daily + + \ No newline at end of file diff --git a/v1/sitemap.xml.gz b/v1/sitemap.xml.gz new file mode 100644 index 000000000..e47e64080 Binary files /dev/null and b/v1/sitemap.xml.gz differ diff --git a/v1/sp-addinhelpers/docs/index.html b/v1/sp-addinhelpers/docs/index.html new file mode 100644 index 000000000..b100c6d53 --- /dev/null +++ b/v1/sp-addinhelpers/docs/index.html @@ -0,0 +1,1771 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sp-addinhelpers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-addinhelpers

+

npm version

+

This module contains classes to allow use of the libraries within a SharePoint add-in.

+

Getting Started

+

Install the library and all dependencies,

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-addinhelpers --save

+

Now you can make requests to the host web from your add-in using the crossDomainWeb method.

+
// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods
+import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers";
+
+// this only needs to be done once within your application
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPRequestExecutorClient();
+        }
+    }
+});
+
+// now we need to use the crossDomainWeb method to make our requests to the host web
+const addInWenUrl = "{The add-in web url, likely from the query string}";
+const hostWebUrl = "{The host web url, likely from the query string}";
+
+// make requests into the host web via the SP.RequestExecutor
+sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => {
+    console.log(JSON.stringify(w, null, 4));
+});
+
+ + +

Libary Topics

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/sp-addinhelpers. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-addinhelpers/docs/sp-request-executor-client/index.html b/v1/sp-addinhelpers/docs/sp-request-executor-client/index.html new file mode 100644 index 000000000..084fdfd78 --- /dev/null +++ b/v1/sp-addinhelpers/docs/sp-request-executor-client/index.html @@ -0,0 +1,1760 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SPRequestExecutorClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-addinhelpers/sprequestexecutorclient

+

The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on +the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request.

+

Setup

+

To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a +SharePoint add-in web.

+
// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods
+import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers";
+
+// this only needs to be done once within your application
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPRequestExecutorClient();
+        }
+    }
+});
+
+// now we need to use the crossDomainWeb method to make our requests to the host web
+const addInWenUrl = "{The add-in web url, likely from the query string}";
+const hostWebUrl = "{The host web url, likely from the query string}";
+
+// make requests into the host web via the SP.RequestExecutor
+sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => {
+    console.log(JSON.stringify(w, null, 4));
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-addinhelpers/docs/sp-rest-addin/index.html b/v1/sp-addinhelpers/docs/sp-rest-addin/index.html new file mode 100644 index 000000000..72c4fff46 --- /dev/null +++ b/v1/sp-addinhelpers/docs/sp-rest-addin/index.html @@ -0,0 +1,1711 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SPRestAddIn - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-addinhelpers/sprestaddin

+

This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls

+
// note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods
+import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers";
+
+// this only needs to be done once within your application
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPRequestExecutorClient();
+        }
+    }
+});
+
+// now we need to use the crossDomainWeb method to make our requests to the host web
+const addInWenUrl = "{The add-in web url, likely from the query string}";
+const hostWebUrl = "{The host web url, likely from the query string}";
+
+// make requests into the host web via the SP.RequestExecutor
+sp.crossDomainWeb(addInWenUrl, hostWebUrl).get().then(w => {
+    console.log(JSON.stringify(w, null, 4));
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-clientsvc/docs/index.html b/v1/sp-clientsvc/docs/index.html new file mode 100644 index 000000000..a1607fbe7 --- /dev/null +++ b/v1/sp-clientsvc/docs/index.html @@ -0,0 +1,1736 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sp-clientsvc - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-clientsvc

+

This library provides base classes for working with the legacy SharePoint client.svc/ProcessQuery endpoint. The base classes support most of the possibilities for types of query calls, as well as supporting fluent batching and caching. They are based on the same @pnp/queryable foundation as the other libraries so should feel familiar when extending. You can see @pnp/sp-taxonomy for an example showing how to extend these base classes into a functional fluent model.

+

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/sp-clientsvc. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-taxonomy/docs/index.html b/v1/sp-taxonomy/docs/index.html new file mode 100644 index 000000000..d6553072b --- /dev/null +++ b/v1/sp-taxonomy/docs/index.html @@ -0,0 +1,1887 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sp-taxonomy - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy

+

npm version

+

This module provides a fluent interface for working with the SharePoint term store. It does not rely on SP.taxonomy.js or other dependencies outside the @pnp scope. It is designed to function in a similar manner and present a similar feel to the other data retrieval libraries. It works by calling the "/_vti_bin/client.svc/ProcessQuery" endpoint.

+

Getting Started

+

You will need to install the @pnp/sp-taxonomy package as well as the packages it requires to run.

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/sp-taxonomy @pnp/sp-clientsvc --save

+

Root Object

+

All fluent taxonomy operations originate from the Taxonomy object. You can access it in several ways.

+

Import existing instance

+

This method will grab an existing instance of the Taxonomy class and allow you to immediately chain additional methods.

+
import { taxonomy } from "@pnp/sp-taxonomy";
+
+await taxonomy.termStores.get();
+
+ + +

Import class and create instance

+

You can also import the Taxonomy class and create a new instance. This useful in those cases where you want to work with taxonomy in another web than the current web.

+
import { Session } from "@pnp/sp-taxonomy";
+
+const taxonomy = new Session("https://mytenant.sharepoint.com/sites/dev");
+
+await taxonomy.termStores.get();
+
+ + +

Setup

+

Because the sp-taxonomy library uses the same @pnp/queryable request pipeline as the other libraries you can call the setup method with the same options used for the @pnp/sp library. The setup method is provided as shorthand and avoids the need to import anything from @pnp/sp if you do not need to. A call to this setup method is equivilent to calling the sp.setup method and the configuration is shared between the libraries within your application.

+

In the below example all requests for the @pnp/sp-taxonomy library and the @pnp/sp library will be routed through the specified SPFetchClient. Sharing the configuration like this handles the most common scenario of working on the same web easily. You can set other values here as well such as baseUrl and they will be respected by both libraries.

+
import { taxonomy } from "@pnp/sp-taxonomy";
+import { SPFetchClient } from "@pnp/nodejs";
+
+// example for setting up the node client using setup method
+// we also set a custom header, as an example
+taxonomy.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPFetchClient("{url}", "{client id}", "{client secret}");
+        },
+        headers: {
+            "X-Custom-Header": "A Great Value",
+        },
+    },
+});
+
+ + +

Library Topics

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/sp-taxonomy. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-taxonomy/docs/labels/index.html b/v1/sp-taxonomy/docs/labels/index.html new file mode 100644 index 000000000..ec247fd16 --- /dev/null +++ b/v1/sp-taxonomy/docs/labels/index.html @@ -0,0 +1,1827 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Labels - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy/labels

+

Load labels

+

You can load labels by accessing the labels property of a term.

+
import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <see terms article for loading term>
+
+// load the terms merged with data
+const labelsWithData: (ILabel & ILabelData)[] = await term.labels.get();
+
+
+// get a label by value
+const label: ILabel = term.labels.getByValue("term value");
+
+// get a label merged with data
+const label2: ILabel & ILabelData = term.labels.getByValue("term value").get();
+
+ + +

Label Properties and Methods

+

setAsDefaultForLanguage

+

Sets this labels as the default for the language

+
import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <see terms article for loading term>
+
+// get a label by value
+await term.labels.getByValue("term value").setAsDefaultForLanguage();
+
+ + +

delete

+

Deletes this label

+
import { ILabel, ILabelData, ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <see terms article for loading term>
+
+// get a label by value
+await term.labels.getByValue("term value").delete();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-taxonomy/docs/term-groups/index.html b/v1/sp-taxonomy/docs/term-groups/index.html new file mode 100644 index 000000000..6b2f4834e --- /dev/null +++ b/v1/sp-taxonomy/docs/term-groups/index.html @@ -0,0 +1,1875 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Term Groups - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy/termgroups

+

Term groups are used as a container for terms within a term store.

+

Load a term group

+

Term groups are loaded from a term store

+
import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + +

Term Group methods and properties

+

addContributor

+

Adds a contributor to the Group

+
import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+await group.addContributor("i:0#.f|membership|person@tenant.com");
+
+ + +

addGroupManager

+

Adds a group manager to the Group

+
import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+await group.addGroupManager("i:0#.f|membership|person@tenant.com");
+
+ + +

createTermSet

+

Creates a new term set

+
import { taxonomy, ITermStore, ITermGroup, ITermSet, ITermSetData } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+const set: ITermSet & ITermSetData = await group.createTermSet("name", 1031);
+
+// you can optionally supply the term set id, if you do not we create a new id for you
+const set2: ITermSet & ITermSetData = await group.createTermSet("name", 1031, "0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + +

get

+

Gets this term group's data

+
import { taxonomy, ITermStore, ITermGroupData, ITermGroup } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup & ITermGroupData = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").get();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-taxonomy/docs/term-sets/index.html b/v1/sp-taxonomy/docs/term-sets/index.html new file mode 100644 index 000000000..9a0b56511 --- /dev/null +++ b/v1/sp-taxonomy/docs/term-sets/index.html @@ -0,0 +1,1946 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Term Sets - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy/termsets

+

Term sets contain terms within the taxonomy heirarchy.

+

Load a term set

+

You load a term set directly from a term store.

+
import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + +

Or you can load a term set from a collection - though if you know the id it is more efficient to get the term set directly.

+
import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set = store.getTermSetsByName("my set", 1031).getById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+const setWithData = await store.getTermSetsByName("my set", 1031).getByName("my set").get();
+
+ + +

Term set methods and properties

+

addStakeholder

+

Adds a stakeholder to the TermSet

+
import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+await set.addStakeholder("i:0#.f|membership|person@tenant.com");
+
+ + +

deleteStakeholder

+

Deletes a stakeholder to the TermSet

+
import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+await set.deleteStakeholder("i:0#.f|membership|person@tenant.com");
+
+ + +

get

+

Gets the data for this TermSet

+
import { taxonomy, ITermStore, ITermSet, ITermSetData } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+const setWithData: ITermSet & ITermSetData = await set.get();
+
+ + +

terms

+

Provides access to the terms collection for this termset

+
import { taxonomy, ITermStore, ITermSet, ITerms, ITermData, ITerm } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+const terms: ITerms = set.terms;
+
+// load the data into the terms instances
+const termsWithData: (ITermData & ITerm)[] = set.terms.get();
+
+ + +

getTermById

+

Gets a term by id from this set

+
import { taxonomy, ITermStore, ITermSet, ITermData, ITerm } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+const term: ITerm = set.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+// load the data into the term instances
+const termWithData: ITermData & ITerm = term.get();
+
+ + +

addTerm

+

Adds a term to a term set

+
import { taxonomy, ITermStore, ITermSet, ITermData, ITerm } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+const term: ITerm & ITermData = await set.addTerm("name", 1031, true);
+
+// you can optionally set the id when you create the term
+const term2: ITerm & ITermData = await set.addTerm("name", 1031, true, "0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + diff --git a/v1/sp-taxonomy/docs/term-stores/index.html b/v1/sp-taxonomy/docs/term-stores/index.html new file mode 100644 index 000000000..e68a993f7 --- /dev/null +++ b/v1/sp-taxonomy/docs/term-stores/index.html @@ -0,0 +1,2111 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Term Stores - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy/termstores

+

Term stores contain term groups, term sets, and terms. This article describes how to work find, load, and use a term store to access the terms inside.

+

List term stores

+

You can access a list of all term stores via the termstores property of the Taxonomy class.

+
// get a list of term stores and return all properties
+const stores = await taxonomy.termStores.get();
+
+// you can also select the fields to return for the term stores using the select operator.
+const stores2 = await taxonomy.termStores.select("Name").get();
+
+ + +

Load a term store

+

To load a specific term store you can use the getByName or getById methods. Using the get method executes the request to the server.

+
const store = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").get();
+
+const store2 = await taxonomy.termStores.getById("f6112509-fba7-4544-b2ed-ce6c9396b646").get();
+
+// you can use select as well with either method to choose the fields to return
+const store3 = await taxonomy.termStores.getById("f6112509-fba7-4544-b2ed-ce6c9396b646").select("Name").get();
+
+ + +

For term stores and all other objects data is returned as a merger of the data and a new instance of the representative class. Allowing you to immediately begin acting on the object. IF you do not need the data, skip the get call until you do.

+
// no data loaded yet, store is an instance of TermStore class
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+// I can call subsequent methods on the same object and will now have an object with data
+// I could have called get above as well - this is just an example
+const store2: ITermStore & ITermStoreData = await store.get();
+
+// log the Name property
+console.log(store2.Name);
+
+// call another TermStore method on the same object
+await store2.addLanguage(1031);
+
+ + +

Term store methods and properties

+

get

+

Loads the data for this term store

+
import { taxonomy, ITermStore } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").get();
+
+ + +

getTermSetsByName

+

Gets the collection of term sets with a matching name

+
import { taxonomy, ITermSets } from "@pnp/sp-taxonomy";
+
+const sets: ITermSets = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==").getTermSetsByName("My Set", 1033);
+
+ + +

getTermSetById

+

Gets the term set with a matching id

+
import { taxonomy, ITermStore, ITermSet } from "@pnp/sp-taxonomy";
+
+// note that you can also use instances if you wanted to conduct multiple operations on a single store
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+const set: ITermSet = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+// we will handle normalizing guids for you as well :)
+const set2: ITermSet = store.getTermSetById("{a63aefc9-359d-42b7-a0d2-cb1809acd260}");
+
+ + +

getTermById

+

Gets a term by id

+
import { taxonomy, ITermStore, ITerm, ITermData } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const term: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+const termWithData: ITerm & ITermData = await term.get();
+
+ + +

getTermsById

+

Added in 1.2.6

+
import { taxonomy, ITermStore, ITerms, ITermData } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const terms: ITerms = store.getTermsById("0ba6845c-1468-4ec5-a5a8-718f1fb05431", "0ba6845c-1468-4ec5-a5a8-718f1fb05432");
+const termWithData: (ITerm & ITermData)[] = await term.get();
+
+ + +

getTermGroupById

+

Gets a term group by id

+
import { taxonomy, ITermStore, ITermGroup } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup = store.getTermGroupById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + +

getTerms

+

Gets terms that match the provided criteria. Please see this article for details on valid querys.

+
import { taxonomy, ITermStore, ILabelMatchInfo, ITerm, ITermData } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const terms: ITerms = store.getTerms({
+                                TermLabel: "test label",
+                                TrimUnavailable: true,
+                            });
+
+// load the data based on the above query
+const termsWithData: (ITerm & ITermData)[] = terms.get();
+
+// select works here too :)
+const termsWithData2: (ITerm & ITermData)[] = terms.select("Name").get();
+
+ + +

addLanguage

+

Adds a language to the term store by LCID

+
import { taxonomy, ITermStore } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+await store.addLanguage(1031);
+
+ + +

addGroup

+

Adds a term group to the term store

+
import { taxonomy, ITermStore } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const group: ITermGroup & ITermGroupData = await store.addGroup("My Group Name");
+
+// you can optionally specify the guid of the group, if you don't we just create a new guid for you
+const groups: ITermGroup & ITermGroupData = await store.addGroup("My Group Name", "0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + +

commitAll

+

Commits all updates to the database that have occurred since the last commit or rollback.

+
import { taxonomy, ITermStore } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+await store.commitAll();
+
+ + +

deleteLanguage

+

Delete a working language from the TermStore

+
import { taxonomy, ITermStore } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+await store.deleteLanguage(1031);
+
+ + +

rollbackAll

+

Discards all updates that have occurred since the last commit or rollback. It is unlikely you will need to call this method through this library due to how things are structured.

+
import { taxonomy, ITermStore } from "@pnp/sp-taxonomy";
+
+const store: ITermStore = taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+await store.rollbackAll();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-taxonomy/docs/terms/index.html b/v1/sp-taxonomy/docs/terms/index.html new file mode 100644 index 000000000..8c079d639 --- /dev/null +++ b/v1/sp-taxonomy/docs/terms/index.html @@ -0,0 +1,2036 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Terms - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy/terms

+

Terms are the individual entries with a term set.

+

Load Terms

+

You can load a collection of terms through a term set or term store.

+
import {
+    taxonomy,
+    ITermStore,
+    ITerms,
+    ILabelMatchInfo,
+    ITerm,
+    ITermData
+} from "@pnp/sp-taxonomy";
+
+const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+const labelMatchInfo: ILabelMatchInfo = {
+    TermLabel: "My Label",
+    TrimUnavailable: true,
+};
+
+const terms: ITerms = store.getTerms(labelMatchInfo);
+
+// get term instances merged with data
+const terms2: (ITermData & ITerm)[] = await store.getTerms(labelMatchInfo).get();
+
+const terms3: ITerms = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").terms;
+
+// get terms merged with data from a term set
+const terms4: (ITerm & ITermData)[] = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").terms.get();
+
+ + +

Load Single Term

+

You can get a single term a variety of ways as shown below. The "best" way will be determined by what information is available to do the lookup but ultimately will result in the same end product.

+
import {
+    taxonomy,
+    ITermStore,
+    ITerms,
+    ILabelMatchInfo,
+    ITerm,
+    ITermData
+} from "@pnp/sp-taxonomy";
+
+const store: ITermStore = await taxonomy.termStores.getByName("Taxonomy_v5o/SbcTE2cegwO2dtAN9l==");
+
+// get a single term by id
+const term: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+// get single get merged with data
+const term2: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").get();
+
+// use select to choose which fields to return
+const term3: ITerm = store.getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").select("Name").get();
+
+// get a term from a term set
+const term4: ITerm = store.getTermSetById("0ba6845c-1468-4ec5-a5a8-718f1fb05431").getTermById("0ba6845c-1468-4ec5-a5a8-718f1fb05431");
+
+ + +

Term methods and properties

+

labels

+

Accesses the labels collection for this term

+
import { taxonomy, ITermStore, ITerm, ILabels } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+const labels: ILabels = term.labels;
+
+// labels merged with data
+const labelsWithData = term.labels.get();
+
+ + +

createLabel

+

Creates a new label for this Term

+
import { taxonomy, ITermStore, ITerm, ILabelData, ILabel } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+const label: ILabelData & ILabel = term.createLabel("label text", 1031);
+
+// optionally specify this is the default label
+const label2: ILabelData & ILabel = term.createLabel("label text", 1031, true);
+
+ + +

deprecate

+

Sets the deprecation flag on a term

+
import { ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+await term.deprecate(true);
+
+ + +

get

+

Loads the term data

+
import { ITerm, ITermData } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+// load term instance merged with data
+const term2: ITerm & ITermData = await term.get();
+
+ + +

getDescription

+

Sets the description

+
import { ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+// load term instance merged with data
+const description = await term.getDescription(1031);
+
+ + +

setDescription

+

Sets the description

+
import { ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+// load term instance merged with data
+await term.setDescription("the description", 1031);
+
+ + +

setLocalCustomProperty

+

Sets a custom property on this term

+
import { ITerm } from "@pnp/sp-taxonomy";
+
+const term: ITerm = <from one of the above methods>;
+
+// load term instance merged with data
+await term.setLocalCustomProperty("name", "value");
+
+ + +

addTerm

+

Added in 1.2.8

+

Adds a child term to an existing term instance.

+
import { ITerm } from "@pnp/sp-taxonomy";
+
+const parentTerm: ITerm = <from one of the above methods>;
+
+await parentTerm.addTerm("child 1", 1033);
+
+await parentTerm.addTerm("child 2", 1033);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp-taxonomy/docs/utilities/index.html b/v1/sp-taxonomy/docs/utilities/index.html new file mode 100644 index 000000000..fdf0ad1e7 --- /dev/null +++ b/v1/sp-taxonomy/docs/utilities/index.html @@ -0,0 +1,1783 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Utilities - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp-taxonomy/utilities

+

These are a collection of helper methods you may find useful.

+

setItemMetaDataField

+

Allows you to easily set the value of a metadata field in a list item.

+
import { sp } from "@pnp/sp";
+import { taxonomy, setItemMetaDataField } from "@pnp/sp-taxonomy";
+
+// create a new item, or load an existing
+const itemResult = await sp.web.lists.getByTitle("TaxonomyList").items.add({
+    Title: "My Title",
+});
+
+// get a term
+const term = await taxonomy.getDefaultSiteCollectionTermStore()
+    .getTermById("99992696-1111-1111-1111-15e65b221111").get();
+
+setItemMetaDataField(itemResult.item, "MetaDataFieldName", term);
+
+ + +

setItemMetaDataMultiField

+

Allows you to easily set the value of a multi-value metadata field in a list item.

+
import { sp } from "@pnp/sp";
+import { taxonomy, setItemMetaDataMultiField } from "@pnp/sp-taxonomy";
+
+// create a new item, or load an existing
+const itemResult = await sp.web.lists.getByTitle("TaxonomyList").items.add({
+    Title: "My Title",
+});
+
+// get a term
+const term = await taxonomy.getDefaultSiteCollectionTermStore()
+    .getTermById("99992696-1111-1111-1111-15e65b221111").get();
+
+// get another term
+const term2 = await taxonomy.getDefaultSiteCollectionTermStore()
+    .getTermById("99992696-1111-1111-1111-15e65b221112").get();
+
+// get yet another term
+const term3 = await taxonomy.getDefaultSiteCollectionTermStore()
+    .getTermById("99992696-1111-1111-1111-15e65b221113").get();
+
+setItemMetaDataMultiField(
+    itemResult.item,
+    "MultiValueMetaDataFieldName",
+    term,
+    term2,
+    term3
+);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/alias-parameters/index.html b/v1/sp/docs/alias-parameters/index.html new file mode 100644 index 000000000..068fd6be1 --- /dev/null +++ b/v1/sp/docs/alias-parameters/index.html @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Alias Parameters - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp - Aliased Parameters

+

Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the +url length limits when working with files and folders.

+

To alias a parameter you include the label name, a separator ("::") and the value in the string. You also need to prepend a "!" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a "@" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use "@p1" you should use "@p2" for a second parameter alias in the same query.

+

Construct a parameter alias

+

Pattern: !@{label name}::{value}

+

Example: "!@p1::\sites\dev" or "!@p2::\text.txt"

+

Example without aliasing

+
import { sp } from "@pnp/sp";
+// still works as expected, no aliasing
+const query = sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files
+console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3
+
+query.get().then(r => {
+
+    console.log(r);
+});
+
+ + +

Example with aliasing

+
import { sp } from "@pnp/sp";
+// same query with aliasing
+const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
+console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
+
+query.get().then(r => {
+
+    console.log(r);
+});
+
+ + +

Example with aliasing and batching

+

Aliasing is supported with batching as well:

+
import { sp } from "@pnp/sp";
+// same query with aliasing and batching
+const batch = sp.web.createBatch();
+
+const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
+
+console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
+console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
+
+query.inBatch(batch).get().then(r => {
+
+    console.log(r);
+});
+
+batch.execute();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/alm/index.html b/v1/sp/docs/alm/index.html new file mode 100644 index 000000000..d5e73d7ef --- /dev/null +++ b/v1/sp/docs/alm/index.html @@ -0,0 +1,1920 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ALM api - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/appcatalog

+

The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.

+

Understanding the App Catalog Heirarchy

+

Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.

+

Reference an App Catalog

+

There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.

+
import { sp } from "@pnp/sp";
+// get the curren't context web's app catalog
+const catalog = sp.web.getAppCatalog();
+
+// you can also chain off the app catalog
+pnp.sp.web.getAppCatalog().get().then(console.log);
+
+ + +
import { sp } from "@pnp/sp";
+// you can get the tenant app catalog (or any app catalog) by passing in a url
+
+// get the tenant app catalog
+const tenantCatalog = sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/appcatalog");
+
+// get a different app catalog
+const catalog = sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/anothersite");
+
+ + +
// alternatively you can create a new app catalog instance directly by importing the AppCatalog class
+import { AppCatalog } from "@pnp/sp";
+
+const catalog = new AppCatalog("https://mytenant.sharepoint.com/sites/dev");
+
+ + +
// and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web
+import { Web, AppCatalog } from "@pnp/sp";
+
+const web = new Web("https://mytenant.sharepoint.com/sites/dev");
+const catalog = new AppCatalog(web);
+
+ + +

The following examples make use of a variable "catalog" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.

+

List Available Apps

+

The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.

+
// get available apps
+catalog.get().then(console.log);
+
+// get available apps selecting two fields
+catalog.select("Title", "Deployed").get().then(console.log);
+
+ + +

Add an App

+

This action must be performed in the context of the tenant app catalog

+
// this represents the file bytes of the app package file
+const blob = new Blob();
+
+// there is an optional third argument to control overwriting existing files
+catalog.add("myapp.app", blob).then(r => {
+
+    // this is at its core a file add operation so you have access to the response data as well
+    // as a File isntance representing the created file
+
+    console.log(JSON.stringify(r.data, null, 4));
+
+    // all file operations are available
+    r.file.select("Name").get().then(console.log);
+});
+
+ + +

Get an App

+

You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions

+
catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").get().then(console.log);
+
+ + +

Perform app actions

+

Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success using then and catch.

+
// deploy
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").deploy().then(console.log).catch(console.error);
+
+// retract
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").retract().then(console.log).catch(console.error);
+
+// install
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").install().then(console.log).catch(console.error);
+
+// uninstall
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").uninstall().then(console.log).catch(console.error);
+
+// upgrade
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").upgrade().then(console.log).catch(console.error);
+
+// remove
+catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c").remove().then(console.log).catch(console.error);
+
+ + +

Notes

+
    +
  • The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.
  • +
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/attachments/index.html b/v1/sp/docs/attachments/index.html new file mode 100644 index 000000000..5067d7c72 --- /dev/null +++ b/v1/sp/docs/attachments/index.html @@ -0,0 +1,1998 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Attachments - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/sp/attachments

+

The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.

+

Get attachments

+
import { sp } from "@pnp/sp";
+
+let item = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+// get all the attachments
+item.attachmentFiles.get().then(v => {
+
+    console.log(v);
+});
+
+// get a single file by file name
+item.attachmentFiles.getByName("file.txt").get().then(v => {
+
+    console.log(v);
+});
+
+// select specific properties using odata operators
+item.attachmentFiles.select("ServerRelativeUrl").get().then(v => {
+
+    console.log(v);
+});
+
+ + +

Add an Attachment

+

You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.

+
import { sp } from "@pnp/sp";
+
+let item = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+item.attachmentFiles.add("file2.txt", "Here is my content").then(v => {
+
+    console.log(v);
+});
+
+ + +

Add Multiple

+

This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining.

+
const list = sp.web.lists.getByTitle("MyList");
+
+var fileInfos: AttachmentFileInfo[] = [];
+
+fileInfos.push({
+    name: "My file name 1",
+    content: "string, blob, or array"
+});
+
+fileInfos.push({
+    name: "My file name 2",
+    content: "string, blob, or array"
+});
+
+list.items.getById(2).attachmentFiles.addMultiple(fileInfos).then(r => {
+
+    console.log(r);
+});
+
+ + +

Delete Multiple

+
const list = sp.web.lists.getByTitle("MyList");
+
+list.items.getById(2).attachmentFiles.deleteMultiple("1.txt","2.txt").then(r => {
+    console.log(r);
+});
+
+ + +

Read Attachment Content

+

You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.

+
import { sp } from "@pnp/sp";
+
+let item = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+item.attachmentFiles.getByName("file.txt").getText().then(v => {
+
+    console.log(v);
+});
+
+// use this in the browser, does not work in nodejs
+item.attachmentFiles.getByName("file.mp4").getBlob().then(v => {
+
+    console.log(v);
+});
+
+// use this in nodejs
+item.attachmentFiles.getByName("file.mp4").getBuffer().then(v => {
+
+    console.log(v);
+});
+
+// file must be valid json
+item.attachmentFiles.getByName("file.json").getJSON().then(v => {
+
+    console.log(v);
+});
+
+ + +

Update Attachment Content

+

You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library.

+
import { sp } from "@pnp/sp";
+
+let item = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+item.attachmentFiles.getByName("file2.txt").setContent("My new content!!!").then(v => {
+
+    console.log(v);
+});
+
+ + +

Delete Attachment

+
import { sp } from "@pnp/sp";
+
+let item = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+item.attachmentFiles.getByName("file2.txt").delete().then(v => {
+
+    console.log(v);
+});
+
+ + +

Recycle Attachment

+

Added in 1.2.4

+

Delete the attachment and send it to recycle bin

+
import { sp } from "@pnp/sp";
+
+let item = sp.web.lists.getByTitle("MyList").items.getById(1);
+
+item.attachmentFiles.getByName("file2.txt").recycle().then(v => {
+
+    console.log(v);
+});
+
+ + +

Recycle Multiple Attachments

+

Added in 1.2.4

+

Delete multiple attachments and send them to recycle bin

+
import { sp } from "@pnp/sp";
+
+const list = sp.web.lists.getByTitle("MyList");
+
+list.items.getById(2).attachmentFiles.recycleMultiple("1.txt","2.txt").then(r => {
+    console.log(r);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/client-side-pages/index.html b/v1/sp/docs/client-side-pages/index.html new file mode 100644 index 000000000..31cb7faa8 --- /dev/null +++ b/v1/sp/docs/client-side-pages/index.html @@ -0,0 +1,2031 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Client-side Pages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/sp/clientsidepages

+

The ability to manage client-side pages is a capability introduced in version 1.0.2 of @pnp/sp. Through the methods described +you can add and edit "modern" pages in SharePoint sites.

+

Add Client-side page

+

Using the addClientSidePage you can add a new client side page to a site, specifying the filename.

+
import { sp } from "@pnp/sp";
+
+const page = await sp.web.addClientSidePage(`file-name`);
+
+// OR
+
+const page = await sp.web.addClientSidePage(`file-name`, `Page Display Title`);
+
+ + +

Added in 1.0.5 you can also add a client side page using the list path. This gets around potential language issues with list title. You must specify the list path when calling this method in addition to the new page's filename.

+
import { sp } from "@pnp/sp";
+
+const page = await sp.web.addClientSidePageByPath(`file-name`, "/sites/dev/SitePages");
+
+ + +

Load Client-side page

+

You can also load an existing page based on the file representing that page. Note that the static fromFile returns a promise which +resolves so the loaded page. Here we are showing use of the getFileByServerRelativeUrl method to get the File instance, but any of the ways +of getting a File instance will work. Also note we are passing the File instance, not the file content.

+
import { 
+    sp,
+    ClientSidePage,
+} from "@pnp/sp";
+
+const page = await ClientSidePage.fromFile(sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/ExistingFile.aspx"));
+
+ + +

The remaining examples below reference a variable "page" which is assumed to be a ClientSidePage instance loaded through one of the above means.

+

Add Controls

+

A client-side page is made up of sections, which have columns, which contain controls. A new page will have none of these and an existing page may have +any combination of these. There are a few rules to understand how sections and columns layout on a page for display. A section is a horizontal piece of +a page that extends 100% of the page width. A page with multiple sections will stack these sections based on the section's order property - a 1 based index.

+

Within a section you can have one or more columns. Each column is ordered left to right based on the column's order property. The width of each column is +controlled by the factor property whose value is one of 0, 2, 4, 6, 8, 10, or 12. The columns in a section should have factors that add up to 12. Meaning +if you wanted to have two equal columns you can set a factor of 6 for each. A page can have empty columns.

+
import { 
+    sp, 
+    ClientSideText, 
+} from "@pnp/sp";
+
+// this code adds a section, and then adds a control to that section. The control is added to the section's defaultColumn, and if there are no columns a single
+// column of factor 12 is created as a default. Here we add the ClientSideText part
+page.addSection().addControl(new ClientSideText("@pnp/sp is a great library!"));
+
+// here we add a section, add two columns, and add a text control to the second section so it will appear on the right of the page
+// add and get a reference to a new section
+const section = page.addSection();
+
+// add a column of factor 6
+section.addColumn(6);
+
+// add and get a reference to a new column of factor 6
+const column = section.addColumn(6);
+
+// add a text control to the second new column
+column.addControl(new ClientSideText("Be sure to check out the @pnp docs at https://pnp.github.io/pnpjs/"));
+
+// we need to save our content changes
+await page.save();
+
+ + +

Add Client-side Web Parts

+

Beyond the text control above you can also add any of the available client-side web parts in a given site. To find out what web parts are available you +first call the web's getClientSideWebParts method. Once you have a list of parts you need to find the defintion you want to use, here we get the Embed web part +whose's id is "490d7c76-1824-45b2-9de3-676421c997fa" (at least in one farm, your mmv).

+
import {
+    sp,
+    ClientSideWebpart,
+    ClientSideWebpartPropertyTypes,
+} from "@pnp/sp";
+
+// this will be a ClientSidePageComponent array
+// this can be cached on the client in production scenarios
+const partDefs = await sp.web.getClientSideWebParts();
+
+// find the definition we want, here by id
+const partDef = partDefs.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa");
+
+// optionally ensure you found the def
+if (partDef.length < 1) {
+    // we didn't find it so we throw an error
+    throw new Error("Could not find the web part");
+}
+
+// create a ClientWebPart instance from the definition
+const part = ClientSideWebpart.fromComponentDef(partDef[0]);
+
+// set the properties on the web part. Here we have imported the ClientSideWebpartPropertyTypes module and can use that to type
+// the available settings object. You can use your own types or help us out and add some typings to the module :).
+// here for the embed web part we only have to supply an embedCode - in this case a youtube video.
+part.setProperties<ClientSideWebpartPropertyTypes.Embed>({
+    embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg",
+});
+
+// we add that part to a new section
+page.addSection().addControl(part);
+
+// save our content changes back to the server
+await page.save();
+
+ + +

Find Controls

+

Added in 1.0.3

+

You can use the either of the two available method to locate controls within a page. These method search through all sections, columns, and controls returning the first instance that meets the supplied criteria.

+
import { ClientSideWebPart } from "@pnp/sp";
+
+// find a control by instance id
+const control1 = page.findControlById("b99bfccc-164e-4d3d-9b96-da48db62eb78");
+
+// type the returned control
+const control2 = page.findControlById<ClientSideWebPart>("c99bfccc-164e-4d3d-9b96-da48db62eb78");
+const control3 = page.findControlById<ClientSideText>("a99bfccc-164e-4d3d-9b96-da48db62eb78");
+
+// use any predicate to find a control
+const control4 = page2.findControl<ClientSideWebpart>((c: CanvasControl) => {
+
+    // any logic you wish can be used on the control here
+    // return true to return that control
+    return c.order > 3;
+});
+
+ + +

Control Comments

+

You can choose to enable or disable comments on a page using these methods

+
// indicates if comments are disabled, not valid until the page is loaded (Added in _1.0.3_)
+page.commentsDisabled
+
+// enable comments
+await page.enableComments();
+
+// disable comments
+await page.disableComments();
+
+ + +

Like/Unlike Client-side page, get like information about page

+

Added in 1.2.4

+

You can like or unlike a modern page. You can also get information about the likes (i.e like Count and which users liked the page)

+
// Like a Client-side page (Added in _1.2.4_)
+await page.like();
+
+// Unlike a Client-side page
+await page.unlike();
+
+// Get liked by information such as like count and user's who liked the page
+await page.getLikedByInformation();
+
+ + +

Sample

+

The below sample shows the process to add a Yammer feed webpart to the page. The properties required as well as the data version are found by adding the part using the UI and reviewing the values. Some or all of these may be discoverable using Yammer APIs. An identical process can be used to add web parts of any type by adjusting the definition, data version, and properties appropriately.

+
// get webpart defs
+const defs = await sp.web.getClientSideWebParts();
+
+// this is the id of the definition in my farm
+const yammerPartDef = defs.filter(d => d.Id === "31e9537e-f9dc-40a4-8834-0e3b7df418bc")[0];
+
+// page file
+const file = sp.web.getFileByServerRelativePath("/sites/dev/SitePages/Testing_kVKF.aspx");
+
+// create page instance
+const page = await ClientSidePage.fromFile(file);
+
+// create part instance from definition
+const part = ClientSideWebpart.fromComponentDef(yammerPartDef);
+
+// update data version
+part.dataVersion = "1.5";
+
+// set the properties required
+part.setProperties({
+    feedType: 0,
+    isSuiteConnected: false,
+    mode: 2,
+    networkId: 9999999,
+    yammerEmbedContainerHeight: 400,
+    yammerFeedURL: "",
+    yammerGroupId: -1,
+    yammerGroupMugshotUrl: "https://mug0.assets-yammer.com/mugshot/images/{width}x{height}/all_company.png",
+    yammerGroupName: "All Company",
+    yammerGroupUrl: "https://www.yammer.com/{tenant}/#/threads/company?type=general",
+});
+
+// add to the section/column you want
+page.sections[0].addControl(part);
+
+// persist changes
+page.save();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/comments-likes/index.html b/v1/sp/docs/comments-likes/index.html new file mode 100644 index 000000000..9c41ad0b4 --- /dev/null +++ b/v1/sp/docs/comments-likes/index.html @@ -0,0 +1,1958 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Comments and Likes - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/comments and likes

+

Likes and comments in the context of modern sites are based on list items, meaning the operations branch from the Item class. To load an item you can refer to the guidance in the items article. If you want to set the likes or comments on a modern page and don't know the item id but do know the url you can first load the file and then use the getItem method to get an item instance:

+

These APIs are currently in BETA and are subject to change or may not work on all tenants.

+
import { sp } from "@pnp/sp";
+
+const item = await sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/Test_8q5L.aspx").getItem();
+
+// as an example, or any of the below options
+await item.like();
+
+ + +

The below examples use a variable named "item" which is taken to represent an instance of the Item class.

+

Comments

+

Get Comments

+
const comments = await item.comments.get();
+
+ + +

You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:

+
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
+
+const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment));
+
+// these will be Comment instances in the array
+comments[0].replies.add({ text: "#PnPjs is pretty ok!" });
+
+//load the top 20 replies and comments for an item including likedBy information
+const comments = await item.comments.expand("replies", "likedBy", "replies/likedBy").top(20).get();
+
+ + +

Add Comment

+
// you can add a comment as a string
+item.comments.add("string comment");
+
+// or you can add it as an object to include mentions
+item.comments.add({ text: "comment from object property" });
+
+ + +

Delete a Comment

+
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
+
+const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment));
+
+// these will be Comment instances in the array
+comments[0].delete()
+
+ + +

Like Comment

+
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
+
+const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment));
+
+// these will be Comment instances in the array
+comments[0].like()
+
+ + +

Unlike Comment

+
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
+
+const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment));
+
+comments[0].unlike()
+
+ + +

Reply to a Comment

+
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
+
+const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment));
+
+const comment: Comment & CommentData = await comments[0].replies.add({ text: "#PnPjs is pretty ok!" });
+
+ + +

Load Replies to a Comment

+
import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
+
+const comments = await item.comments.get(spODataEntityArray<Comment, CommentData>(Comment));
+
+const replies = await comments[0].replies.get();
+
+ + +

Like

+

You can like items and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.

+
import { LikeData } from "@pnp/sp";
+
+// like an item
+await item.like();
+
+// unlike an item
+await item.unlike();
+
+// get the liked by information
+const likedByData: LikeData[] = await item.getLikedBy();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/content-types/index.html b/v1/sp/docs/content-types/index.html new file mode 100644 index 000000000..e575d1461 --- /dev/null +++ b/v1/sp/docs/content-types/index.html @@ -0,0 +1,1758 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Content Types - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/content types

+

Set Folder Unique Content Type Order

+
interface OrderData {
+    ContentTypeOrder: { StringValue: string }[];
+    UniqueContentTypeOrder?: { StringValue: string }[];
+}
+
+const folder = sp.web.lists.getById("{list id guid}").rootFolder;
+
+// here you need to see if there are unique content type orders already or just the default
+const existingOrders = await folder.select("ContentTypeOrder", "UniqueContentTypeOrder").get<OrderData>();
+
+const activeOrder = existingOrders.UniqueContentTypeOrder ? existingOrders.UniqueContentTypeOrder : existingOrders.ContentTypeOrder;
+
+// manipulate the order here however you want (I am just reversing the array as an example)
+const newOrder = activeOrder.reverse();
+
+// update the content type order thusly:
+await folder.update({
+    UniqueContentTypeOrder: {
+        __metadata: { type: "Collection(SP.ContentTypeId)" },
+        results: newOrder,
+    },
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/entity-merging/index.html b/v1/sp/docs/entity-merging/index.html new file mode 100644 index 000000000..59e7d4ca2 --- /dev/null +++ b/v1/sp/docs/entity-merging/index.html @@ -0,0 +1,1838 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Entity Merging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp - entity merging

+

Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its represending type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples.

+

Request a single entity

+

If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query.

+
import { sp, spODataEntity, Item } from "@pnp/sp";
+
+// interface defining the returned properites
+interface MyProps {
+    Id: number;
+}
+
+try {
+
+    // get a list item laoded with data and merged into an instance of Item
+    const item = await sp.web.lists.getByTitle("ListTitle").items.getById(1).get(spODataEntity<Item, MyProps>(Item));
+
+    // log the item id, all properties specified in MyProps will be type checked
+    Logger.write(`Item id: ${item.Id}`);
+
+    // now we can call update because we have an instance of the Item type to work with as well
+    await item.update({
+        Title: "New title.",
+    });
+
+} catch (e) {
+    Logger.error(e);
+}
+
+ + +

Request a collection

+

The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method.

+
import { sp, spODataEntityArray, Item } from "@pnp/sp";
+
+// interface defining the returned properites
+interface MyProps {
+    Id: number;
+    Title: string;
+}
+
+try {
+
+    // get a list item laoded with data and merged into an instance of Item
+    const items = await sp.web.lists.getByTitle("ListTitle").items.select("Id", "Title").get(spODataEntityArray<Item, MyProps>(Item));
+
+    Logger.write(`Item id: ${items.length}`);
+
+    Logger.write(`Item id: ${items[0].Title}`);
+
+    // now we can call update because we have an instance of the Item type to work with as well
+    await items[0].update({
+        Title: "New title.",
+    });
+
+} catch (e) {
+
+    Logger.error(e);
+}
+
+ + +

Use with Item getPaged

+

Added in 1.3.4

+

Starting with 1.3.4 you can now include entity merging in the getPaged command as shown below. This approach will work with any objects matching the required factory pattern.

+
// create Item instances with the defined property Title
+const items = await sp.web.lists.getByTitle("BigList").items.select("Title").getPaged(spODataEntityArray<Item, { Title: string }>(Item));
+
+console.log(items.results.length);
+
+// now invoke methods on the Item object
+const perms = await items.results[0].getCurrentUserEffectivePermissions();
+
+console.log(JSON.stringify(perms, null, 2));
+
+// you can also type the result slightly differently if you prefer this, but the results are the same functionally.
+const items2 = await sp.web.lists.getByTitle("BigList").items.select("Title").getPaged<(Item & { Title: string })[]>(spODataEntityArray(Item));
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/features/index.html b/v1/sp/docs/features/index.html new file mode 100644 index 000000000..2be7fc17c --- /dev/null +++ b/v1/sp/docs/features/index.html @@ -0,0 +1,1824 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Features - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/features

+

Features are used by SharePoint to package a set of functionality and either enable (activate) or disable (deactivate) that functionality based on requirements for a specific site. You can manage feature activation using the library as shown below. Note that the features collection only contains active features.

+

List all Features

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+// get all the active features
+web.features.get().then(f => {
+
+    console.log(f);
+});
+
+// select properties using odata operators
+web.features.select("DisplayName", "DefinitionId").get().then(f => {
+
+    console.log(f);
+});
+
+// get a particular feature by id
+web.features.getById("87294c72-f260-42f3-a41b-981a2ffce37a").select("DisplayName", "DefinitionId").get().then(f => {
+
+    console.log(f);
+});
+
+// get features using odata operators
+web.features.filter("DisplayName eq 'MDSFeature'").get().then(f => {
+
+    console.log(f);
+});
+
+ + +

Activate a Feature

+

To activate a feature you must know the feature id. You can optionally force activation - if you aren't sure don't use force.

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+// activate the minimum download strategy feature
+web.features.add("87294c72-f260-42f3-a41b-981a2ffce37a").then(f => {
+
+    console.log(f);
+});
+
+ + +

Deactivate a Feature

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+web.features.remove("87294c72-f260-42f3-a41b-981a2ffce37a").then(f => {
+
+    console.log(f);
+});
+
+// you can also deactivate a feature but going through the collection's remove method is faster
+web.features.getById("87294c72-f260-42f3-a41b-981a2ffce37a").deactivate().then(f => {
+
+    console.log(f);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/fields/index.html b/v1/sp/docs/fields/index.html new file mode 100644 index 000000000..f038259e4 --- /dev/null +++ b/v1/sp/docs/fields/index.html @@ -0,0 +1,2031 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fields - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/fields

+

Fields allow you to store typed information within a SharePoint list. There are many types of fields and the library seeks to simplify working with the most common types. Fields exist in both site collections (site columns) or lists (list columns) and you can add/modify/delete them at either of these levels.

+

Get Fields

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+// get all the fields in a web
+web.fields.get().then(f => {
+
+    console.log(f);
+});
+
+// you can use odata operators on the fields collection
+web.fields.select("Title", "InternalName", "TypeAsString").top(10).orderBy("Id").get().then(f => {
+
+    console.log(f);
+});
+
+// get all the available fields in a web (includes parent web's fields)
+web.availablefields.get().then(f => {
+
+    console.log(f);
+});
+
+// get the fields in a list
+web.lists.getByTitle("MyList").fields.get().then(f => {
+
+    console.log(f);
+});
+
+// you can also get individual fields using getById, getByTitle, or getByInternalNameOrTitle
+web.fields.getById("dee9c205-2537-44d6-94e2-7c957e6ebe6e").get().then(f => {
+
+    console.log(f);
+});
+
+web.fields.getByTitle("MyField4").get().then(f => {
+
+    console.log(f);
+});
+
+web.fields.getByInternalNameOrTitle("MyField4").get().then(f => {
+
+    console.log(f);
+});
+
+ + +

Filtering Fields

+

Sometimes you only want a subset of fields from the collection. Below are some examples of using the filter operator with the fields collection.

+
import { sp } from '@pnp/sp';
+
+const list = sp.web.lists.getByTitle('Custom');
+
+// Fields which can be updated
+const filter1 = `Hidden eq false and ReadOnlyField eq false`;
+list.fields.select('InternalName').filter(filter1).get().then(fields => {
+    console.log(`Can be updated: ${fields.map(f => f.InternalName).join(', ')}`);
+    // Title, ...Custom, ContentType, Attachments
+});
+
+// Only custom field
+const filter2 = `Hidden eq false and CanBeDeleted eq true`;
+list.fields.select('InternalName').filter(filter2).get().then(fields => {
+    console.log(`Custom fields: ${fields.map(f => f.InternalName).join(', ')}`);
+    // ...Custom
+});
+
+// Application specific fields
+const includeFields = [ 'Title', 'Author', 'Editor', 'Modified', 'Created' ];
+const filter3 = `Hidden eq false and (ReadOnlyField eq false or (${
+    includeFields.map(field => `InternalName eq '${field}'`).join(' or ')
+}))`;
+list.fields.select('InternalName').filter(filter3).get().then(fields => {
+    console.log(`Application specific: ${fields.map(f => f.InternalName).join(', ')}`);
+    // Title, ...Custom, ContentType, Modified, Created, Author, Editor, Attachments
+});
+
+// Fields in a view
+list.defaultView.fields.select('Items').get().then(f => {
+    const fields = (f as any).Items.results || (f as any).Items;
+    console.log(`Fields in a view: ${fields.join(', ')}`);
+});
+
+ + +

Add Fields

+

You can add fields using the add, createFieldAsXml, or one of the type specific methods. Functionally there is no difference, however one method may be easier given a certain scenario.

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+// if you use add you _must_ include the correct FieldTypeKind in the extended properties
+web.fields.add("MyField1", "SP.FieldText", { 
+    Group: "~Example",
+    FieldTypeKind: 2,
+    Filterable: true,
+    Hidden: false,
+    EnforceUniqueValues: true,
+}).then(f => {
+
+    console.log(f);
+});
+
+// you can also use the addText or any of the other type specific methods on the collection
+web.fields.addText("MyField2", 75, { 
+    Group: "~Example"
+}).then(f => {
+
+    console.log(f);
+});
+
+// if you have the field schema (for example from an old elements file) you can use createFieldAsXml
+let xml = `<Field DisplayName="MyField4" Type="Text" Required="FALSE" StaticName="MyField4" Name="MyField4" MaxLength="125" Group="~Example" />`;
+
+web.fields.createFieldAsXml(xml).then(f => {
+
+    console.log(f);
+});
+
+// the same operations work on a list's fields collection
+web.lists.getByTitle("MyList").fields.addText("MyField5", 100).then(f => {
+
+    console.log(f);
+});
+
+// Create a lookup field, and a dependent lookup field
+web.lists.getByTitle("MyList").fields.addLookup("MyLookup", "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx", "MyLookupTargetField").then(f => {
+    console.log(f);
+
+    // Create the dependent lookup field
+    return web.lists.getByTitle("MyList").fields.addDependentLookupField("MyLookup_ID", f.Id, "ID");
+}).then(fDep => {
+    console.log(fDep);
+});
+
+ + +

Adding Multiline Text Fields with FullHtml

+

Because the RichTextMode property is not exposed to the clients we cannot set this value via the API directly. The work around is to use the createFieldAsXml method as shown below

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+const fieldAddResult = await web.fields.createFieldAsXml(`<Field Type="Note" Name="Content" DisplayName="Content" Required="{TRUE|FALSE}" RichText="TRUE" RichTextMode="FullHtml" />`);
+
+ + +

Update a Field

+

You can also update the properties of a field in both webs and lists, but not all properties are able to be updated after creation. You can review this list for details.

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+web.fields.getByTitle("MyField4").update({ 
+    Description: "A new description",
+ }).then(f => {
+
+    console.log(f);
+});
+
+ + +

Update a Url/Picture Field

+

When updating a URL or Picture field you need to include the __metadata descriptor as shown below.

+
import { sp } from "@pnp/sp";
+
+const data = {
+    "My_Field_Name": {
+        "__metadata": { "type": "SP.FieldUrlValue" },
+        "Description": "A Pretty picture",
+        "Url": "https://tenant.sharepoint.com/sites/dev/Style%20Library/DSC_0024.JPG",
+    },
+};
+
+await sp.web.lists.getByTitle("MyListTitle").items.getById(1).update(data);
+
+ + +

Delete a Field

+
import { sp } from "@pnp/sp";
+
+let web = sp.web;
+
+web.fields.getByTitle("MyField4").delete().then(f => {
+
+    console.log(f);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/files/index.html b/v1/sp/docs/files/index.html new file mode 100644 index 000000000..85183647c --- /dev/null +++ b/v1/sp/docs/files/index.html @@ -0,0 +1,2178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Files - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/files

+

One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.

+

Reading Files

+

Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.

+
import { sp } from "@pnp/sp";
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBlob().then((blob: Blob) => {});
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBuffer().then((buffer: ArrayBuffer) => {});
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.json").getJSON().then((json: any) => {});
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.txt").getText().then((text: string) => {});
+
+// all of these also work from a file object no matter how you access it
+sp.web.getFolderByServerRelativeUrl("/sites/dev/documents").files.getByName("file.txt").getText().then((text: string) => {});
+
+ + +

Adding Files

+

Likewise you can add files using one of two methods, add or addChunked. The second is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.

+
declare var require: (s: string) => any;
+
+import { ConsoleListener, Web, Logger, LogLevel, ODataRaw } from "@pnp/sp";
+import { auth } from "./auth";
+let $ = require("jquery");
+
+let siteUrl = "https://mytenant.sharepoint.com/sites/dev";
+
+// comment this out for non-node execution
+// auth(siteUrl);
+
+Logger.subscribe(new ConsoleListener());
+Logger.activeLogLevel = LogLevel.Verbose;
+
+let web = new Web(siteUrl);
+
+$(() => {
+    $("#testingdiv").append("<button id='thebuttontodoit'>Do It</button>");
+
+    $("#thebuttontodoit").on('click', (e) => {
+
+        e.preventDefault();
+
+        let input = <HTMLInputElement>document.getElementById("thefileinput");
+        let file = input.files[0];
+
+        // you can adjust this number to control what size files are uploaded in chunks
+        if (file.size <= 10485760) {
+
+            // small upload
+            web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true).then(_ => Logger.write("done"));
+        } else {
+
+            // large upload
+            web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.addChunked(file.name, file, data => {
+
+                Logger.log({ data: data, level: LogLevel.Verbose, message: "progress" });
+
+            }, true).then(_ => Logger.write("done!"));
+        }
+    });
+});
+
+ + +

Setting Associated Item Values

+

You can also update the file properties of a newly uploaded file using code similar to the below snippet:

+
import { sp } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true).then(f => {
+
+    f.file.getItem().then(item => {
+
+        item.update({
+            Title: "A Title",
+            OtherField: "My Other Value"
+        });
+    });
+});
+
+ + +

Update File Content

+

You can of course use similar methods to update existing files as shown below:

+
import { sp } from "@pnp/sp";
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.txt").setContent("New string content for the file.");
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.mp4").setContentChunked(file);
+
+ + +

Check in, Check out, and Approve & Deny

+

The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.

+

Check In

+

Check in takes two optional arguments, comment and check in type.

+
import { sp, CheckinType } from "@pnp/sp";
+
+// default options with empty comment and CheckinType.Major
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin().then(_ => {
+
+    console.log("File checked in!");
+});
+
+// supply a comment (< 1024 chars) and using default check in type CheckinType.Major
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment").then(_ => {
+
+    console.log("File checked in!");
+});
+
+// Supply both comment and check in type
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment", CheckinType.Overwrite).then(_ => {
+
+    console.log("File checked in!");
+});
+
+ + +

Check Out

+

Check out takes no arguments.

+
import { sp } from "@pnp/sp";
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkout().then(_ => {
+
+    console.log("File checked out!");
+});
+
+ + +

Approve and Deny

+

You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.

+
import { sp } from "@pnp/sp";
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").approve("Approval Comment").then(_ => {
+
+    console.log("File approved!");
+});
+
+// deny with no comment
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny().then(_ => {
+
+    console.log("File denied!");
+});
+
+// deny with a supplied comment.
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny("Deny comment").then(_ => {
+
+    console.log("File denied!");
+});
+
+ + +

Publish and Unpublish

+

You can both publish and unpublish a file using the library. Both methods take an optional comment argument.

+
import { sp } from "@pnp/sp";
+// publish with no comment
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish().then(_ => {
+
+    console.log("File published!");
+});
+
+// publish with a supplied comment.
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish("Publish comment").then(_ => {
+
+    console.log("File published!");
+});
+
+// unpublish with no comment
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish().then(_ => {
+
+    console.log("File unpublished!");
+});
+
+// unpublish with a supplied comment.
+sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish("Unpublish comment").then(_ => {
+
+    console.log("File unpublished!");
+});
+
+ + +

Advanced Upload Options

+

Both the addChunked and setContentChunked methods support options beyond just supplying the file content.

+

progress function

+

A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:

+

(data: ChunkedFileUploadProgressData) => void

+

The data interface is:

+
export interface ChunkedFileUploadProgressData {
+    stage: "starting" | "continue" | "finishing";
+    blockNumber: number;
+    totalBlocks: number;
+    chunkSize: number;
+    currentPointer: number;
+    fileSize: number;
+}
+
+ + +

chunkSize

+

This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.

+

getItem

+

This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.

+
import { sp } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem().then(item => {
+
+    console.log(item);
+});
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem("Title", "Modified").then(item => {
+
+    console.log(item);
+});
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem().then(item => {
+
+    // you can also chain directly off this item instance
+    item.getCurrentUserEffectivePermissions().then(perms => {
+
+        console.log(perms);
+    });
+});
+
+ + +

You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.

+
import { sp } from "@pnp/sp";
+// also supports typing the objects so your type will be a union type
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getItem<{ Id: number, Title: string }>("Id", "Title").then(item => {
+
+    // You get intellisense and proper typing of the returned object
+    console.log(`Id: ${item.Id} -- ${item.Title}`);
+
+    // You can also chain directly off this item instance
+    item.getCurrentUserEffectivePermissions().then(perms => {
+
+        console.log(perms);
+    });
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/index.html b/v1/sp/docs/index.html new file mode 100644 index 000000000..9be93ee65 --- /dev/null +++ b/v1/sp/docs/index.html @@ -0,0 +1,1894 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + sp - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp

+

npm version

+

This package contains the fluent api used to call the SharePoint rest services.

+

Getting Started

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save

+

Import the library into your application and access the root sp object

+
import { sp } from "@pnp/sp";
+
+(function main() {
+
+    // here we will load the current web's title
+    sp.web.select("Title").get().then(w => {
+
+        console.log(`Web Title: ${w.Title}`);
+    });
+})()
+
+ + +

Getting Started: SharePoint Framework

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp --save

+

Import the library into your application, update OnInit, and access the root sp object in render

+
import { sp } from "@pnp/sp";
+
+// ...
+
+public onInit(): Promise<void> {
+
+  return super.onInit().then(_ => {
+
+    // other init code may be present
+
+    sp.setup({
+      spfxContext: this.context
+    });
+  });
+}
+
+// ...
+
+public render(): void {
+
+    // A simple loading message
+    this.domElement.innerHTML = `Loading...`;
+
+    sp.web.select("Title").get().then(w => {
+
+        this.domElement.innerHTML = `Web Title: ${w.Title}`;
+    });
+}
+
+ + +

Getting Started: Nodejs

+

Install the library and required dependencies

+

npm install @pnp/logging @pnp/core @pnp/queryable @pnp/sp @pnp/nodejs --save

+

Import the library into your application, setup the node client, make a request

+
import { sp } from "@pnp/sp";
+import { SPFetchClient } from "@pnp/nodejs";
+
+// do this once per page load
+sp.setup({
+    sp: {
+        fetchClientFactory: () => {
+            return new SPFetchClient("{your site url}", "{your client id}", "{your client secret}");
+        },
+    },
+});
+
+// now make any calls you need using the configured client
+sp.web.select("Title").get().then(w => {
+
+    console.log(`Web Title: ${w.Title}`);
+});
+
+ + +

Library Topics

+ +

UML

+

Graphical UML diagram

+

Graphical UML diagram of @pnp/sp. Right-click the diagram and open in new tab if it is too small.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/items/index.html b/v1/sp/docs/items/index.html new file mode 100644 index 000000000..ad1cceaaf --- /dev/null +++ b/v1/sp/docs/items/index.html @@ -0,0 +1,2362 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + List Items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/sp/items

+

GET

+

Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.

+

Basic Get

+
import { sp } from "@pnp/sp";
+
+// get all the items from a list
+sp.web.lists.getByTitle("My List").items.get().then((items: any[]) => {
+    console.log(items);
+});
+
+// get a specific item by id
+sp.web.lists.getByTitle("My List").items.getById(1).get().then((item: any) => {
+    console.log(item);
+});
+
+// use odata operators for more efficient queries
+sp.web.lists.getByTitle("My List").items.select("Title", "Description").top(5).orderBy("Modified", true).get().then((items: any[]) => {
+    console.log(items);
+});
+
+ + +

Get Paged Items

+

Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.

+
import { sp } from "@pnp/sp";
+
+// basic case to get paged items form a list
+let items = await sp.web.lists.getByTitle("BigList").items.getPaged();
+
+// you can also provide a type for the returned values instead of any
+let items = await sp.web.lists.getByTitle("BigList").items.getPaged<{Title: string}[]>();
+
+// the query also works with select to choose certain fields and top to set the page size
+let items = await sp.web.lists.getByTitle("BigList").items.select("Title", "Description").top(50).getPaged<{Title: string}[]>();
+
+// the results object will have two properties and one method:
+
+// the results property will be an array of the items returned
+if (items.results.length > 0) {
+    console.log("We got results!");
+
+    for (let i = 0; i < items.results.length; i++) {
+        // type checking works here if we specify the return type
+        console.log(items.results[i].Title);
+    }
+}
+
+// the hasNext property is used with the getNext method to handle paging
+// hasNext will be true so long as there are additional results
+if (items.hasNext) {
+
+    // this will carry over the type specified in the original query for the results array
+    items = await items.getNext();
+    console.log(items.results.length);
+}
+
+ + +

getListItemChangesSinceToken

+

The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.

+
import { sp } from "@pnp/sp";
+
+// Using RowLimit. Enables paging
+let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({RowLimit: '5'});
+
+// Use QueryOptions to make a XML-style query.
+// Because it's XML we need to escape special characters
+// Instead of & we use &amp; in the query
+let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext="Paged=TRUE&amp;p_ID=5" />'});
+
+// Get everything. Using null with ChangeToken gets everything
+let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({ChangeToken: null});
+
+ + +

Get All Items

+

Added in 1.0.2

+

Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should +be used.

+
import { sp } from "@pnp/sp";
+// basic usage
+sp.web.lists.getByTitle("BigList").items.getAll().then((allItems: any[]) => {
+
+    // how many did we get
+    console.log(allItems.length);
+});
+
+// set page size
+sp.web.lists.getByTitle("BigList").items.getAll(4000).then((allItems: any[]) => {
+
+    // how many did we get
+    console.log(allItems.length);
+});
+
+// use select and top. top will set page size and override the any value passed to getAll
+sp.web.lists.getByTitle("BigList").items.select("Title").top(4000).getAll().then((allItems: any[]) => {
+
+    // how many did we get
+    console.log(allItems.length);
+});
+
+// we can also use filter as a supported odata operation, but this will likely fail on large lists
+sp.web.lists.getByTitle("BigList").items.select("Title").filter("Title eq 'Test'").getAll().then((allItems: any[]) => {
+
+    // how many did we get
+    console.log(allItems.length);
+});
+
+ + +

Retrieving Lookup Fields

+

When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.

+
import { sp } from "@pnp/sp";
+
+sp.web.lists.getByTitle("LookupList").items.select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup").get().then((items: any[]) => {
+    console.log(items);
+});
+
+sp.web.lists.getByTitle("LookupList").items.getById(1).select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup").get().then((item: any) => {
+    console.log(item);
+});
+
+ + +

Retrieving PublishingPageImage

+

The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.

+
import { Web } from "@pnp/sp";
+
+const w = new Web("https://{publishing site url}");
+
+w.lists.getByTitle("Pages").items
+    .select("Title", "FileRef", "FieldValuesAsText/MetaInfo")
+    .expand("FieldValuesAsText")
+    .get().then(r => {
+
+        // look through the returned items.
+        for (var i = 0; i < r.length; i++) {
+
+            // the title field value
+            console.log(r[i].Title);
+
+            // find the value in the MetaInfo string using regex
+            const matches = /PublishingPageImage:SW\|(.*?)\r\n/ig.exec(r[i].FieldValuesAsText.MetaInfo);
+            if (matches !== null && matches.length > 1) {
+
+                // this wil be the value of the PublishingPageImage field
+                console.log(matches[1]);
+            }
+        }
+    }).catch(e => { console.error(e); });
+
+ + +

Add Items

+

There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.

+
import { sp, ItemAddResult } from "@pnp/sp";
+
+// add an item to the list
+sp.web.lists.getByTitle("My List").items.add({
+    Title: "Title",
+    Description: "Description"
+}).then((iar: ItemAddResult) => {
+    console.log(iar);
+});
+
+ + +

Content Type

+

You can also set the content type id when you create an item as shown in the example below:

+
import { sp } from "@pnp/sp";
+
+sp.web.lists.getById("4D5A36EA-6E84-4160-8458-65C436DB765C").items.add({
+    Title: "Test 1",
+    ContentTypeId: "0x01030058FD86C279252341AB303852303E4DAF"
+});
+
+ + +

User Fields

+

There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with "Id" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.

+

Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a "results" property and an array. Examples for both are shown below.

+
import { sp } from "@pnp/sp";
+import { getGUID } from "@pnp/core";
+
+sp.web.lists.getByTitle("PeopleFields").items.add({
+    Title: getGUID(),
+    User1Id: 9, // allows a single user
+    User2Id: {
+        results: [ 16, 45 ] // allows multiple users
+    }
+}).then(i => {
+    console.log(i);
+});
+
+ + +

If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.

+
import { sp } from "@pnp/sp";
+
+const result = await sp.web.lists.getByTitle("UserFieldList").items.getById(1).validateUpdateListItem([{
+    FieldName: "UserField",
+    FieldValue: JSON.stringify([{ "Key": "i:0#.f|membership|person@tenant.com" }]),
+},
+{
+    FieldName: "Title",
+    FieldValue: "Test - Updated",
+}]);
+
+ + +

Lookup Fields

+

What is said for User Fields is, in general, relevant to Lookup Fields: +- Lookup Field types: + - Single-valued lookup + - Multiple-valued lookup +- Id suffix should be appended to the end of lookup's EntityPropertyName in payloads +- Numeric Ids for lookups' items should be passed as values

+
import { sp } from "@pnp/sp";
+import { getGUID } from "@pnp/core";
+
+sp.web.lists.getByTitle("LookupFields").items.add({
+    Title: getGUID(),
+    LookupFieldId: 2,       // allows a single lookup value
+    MuptiLookupFieldId: {
+        results: [ 1, 56 ]  // allows multiple lookup value
+    }
+}).then(console.log).catch(console.log);
+
+ + +

Add Multiple Items

+
import { sp } from "@pnp/sp";
+
+let list = sp.web.lists.getByTitle("rapidadd");
+
+list.getListItemEntityTypeFullName().then(entityTypeFullName => {
+
+    let batch = sp.web.createBatch();
+
+    list.items.inBatch(batch).add({ Title: "Batch 6" }, entityTypeFullName).then(b => {
+        console.log(b);
+    });
+
+    list.items.inBatch(batch).add({ Title: "Batch 7" }, entityTypeFullName).then(b => {
+        console.log(b);
+    });
+
+    batch.execute().then(d => console.log("Done"));
+});
+
+ + +

Update

+

The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.

+
import { sp } from "@pnp/sp";
+
+let list = sp.web.lists.getByTitle("MyList");
+
+list.items.getById(1).update({
+    Title: "My New Title",
+    Description: "Here is a new description"
+}).then(i => {
+    console.log(i);
+});
+
+ + +

Getting and updating a collection using filter

+
import { sp } from "@pnp/sp";
+
+// you are getting back a collection here
+sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'").get().then((items: any[]) => {
+    // see if we got something
+    if (items.length > 0) {
+        sp.web.lists.getByTitle("MyList").items.getById(items[0].Id).update({
+            Title: "Updated Title",
+        }).then(result => {
+            // here you will have updated the item
+            console.log(JSON.stringify(result));
+        });
+    }
+});
+
+ + +

Update Multiple Items

+

This approach avoids multiple calls for the same list's entity type name.

+
import { sp } from "@pnp/sp";
+
+let list = sp.web.lists.getByTitle("rapidupdate");
+
+list.getListItemEntityTypeFullName().then(entityTypeFullName => {
+
+    let batch = sp.web.createBatch();
+
+    // note requirement of "*" eTag param - or use a specific eTag value as needed
+    list.items.getById(1).inBatch(batch).update({ Title: "Batch 6" }, "*", entityTypeFullName).then(b => {
+        console.log(b);
+    });
+
+    list.items.getById(2).inBatch(batch).update({ Title: "Batch 7" }, "*", entityTypeFullName).then(b => {
+        console.log(b);
+    });
+
+    batch.execute().then(d => console.log("Done"));
+});
+
+ + +

Recycle

+

Sending an item to the Recycle Bin is as simple as calling the .recycle method.

+
import { sp } from "@pnp/sp";
+
+let list = sp.web.lists.getByTitle("MyList");
+
+list.items.getById(1).recycle().then(_ => {});
+
+ + +

Delete

+

Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.

+
import { sp } from "@pnp/sp";
+
+let list = sp.web.lists.getByTitle("MyList");
+
+list.items.getById(1).delete().then(_ => {});
+
+ + +

Resolving field names

+

It's a very common mistake trying wrong field names in the requests. +Field's EntityPropertyName value should be used.

+

The easiest way to get know EntityPropertyName is to use the following snippet:

+
import { sp } from "@pnp/sp";
+
+sp.web.lists
+  .getByTitle('[Lists_Title]')
+  .fields
+  .select('Title, EntityPropertyName')
+  .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`)
+  .get()
+  .then(response => {
+    console.log(response.map(field => {
+      return {
+        Title: field.Title,
+    EntityPropertyName: field.EntityPropertyName
+      };
+    }));
+  })
+  .catch(console.log);
+
+ + +

Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/navigation-service/index.html b/v1/sp/docs/navigation-service/index.html new file mode 100644 index 000000000..b625c31e7 --- /dev/null +++ b/v1/sp/docs/navigation-service/index.html @@ -0,0 +1,1791 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Navigation Service - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/navigation service

+

The global navigation service located at "_api/navigation" provides access to the SiteMapProvider instances available in a given site collection.

+

getMenuState

+

The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.

+

The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma seperated string of property names like: property1,property2,property3\,containingcomma

+

NOTE: the , seperator can be escaped using the \ as escape character as done in the example above. The string above would split like: + property1 + property2 +* property3,containingcomma

+
import { sp } from "@pnp/sp";
+
+// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels.
+sp.navigation.getMenuState().then(r => {
+
+    console.log(JSON.stringify(r, null, 4));
+
+}).catch(console.error);
+
+// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5
+sp.navigation.getMenuState("1002", 5).then(r => {
+
+    console.log(JSON.stringify(r, null, 4));
+
+}).catch(console.error);
+
+// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5
+sp.navigation.getMenuState(null, 5, "CurrentNavSiteMapProviderNoEncode").then(r => {
+
+    console.log(JSON.stringify(r, null, 4));
+
+}).catch(console.error);
+
+ + +

getMenuNodeKey

+

Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.

+
import { sp } from "@pnp/sp";
+
+sp.navigation.getMenuNodeKey("/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx").then(r => {
+
+    console.log(JSON.stringify(r, null, 4));
+
+}).catch(console.error);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/permissions/index.html b/v1/sp/docs/permissions/index.html new file mode 100644 index 000000000..b1d09ae99 --- /dev/null +++ b/v1/sp/docs/permissions/index.html @@ -0,0 +1,1860 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Permissions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp - permissions

+

A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.

+

Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.

+

Get Role Assignments

+

This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.

+
import { sp } from "@pnp/sp";
+import { Logger } from "@pnp/logging";
+
+sp.web.roleAssignments.get().then(roles => {
+
+    Logger.writeJSON(roles);
+});
+
+ + +

First Unique Ancestor Securable Object

+

This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.

+
import { sp } from "@pnp/sp";
+import { Logger } from "@pnp/logging";
+
+sp.web.firstUniqueAncestorSecurableObject.get().then(obj => {
+
+    Logger.writeJSON(obj);
+});
+
+ + +

User Effective Permissions

+

This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.

+
import { sp } from "@pnp/sp";
+import { Logger } from "@pnp/logging";
+
+sp.web.getUserEffectivePermissions("i:0#.f|membership|user@site.com").then(perms => {
+
+    Logger.writeJSON(perms);
+});
+
+sp.web.getCurrentUserEffectivePermissions().then(perms => {
+
+    Logger.writeJSON(perms);
+});
+
+ + +

User Has Permissions

+

Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.

+
import { sp, PermissionKind } from "@pnp/sp";
+
+sp.web.userHasPermissions("i:0#.f|membership|user@site.com", PermissionKind.ApproveItems).then(perms => {
+
+    console.log(perms);
+});
+
+sp.web.currentUserHasPermissions(PermissionKind.ApproveItems).then(perms => {
+
+    console.log(perms);
+});
+
+ + +

Has Permissions

+

If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.

+
import { sp, PermissionKind } from "@pnp/sp";
+
+sp.web.getCurrentUserEffectivePermissions().then(perms => {
+
+    if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) {
+        // ...
+    }
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/profiles/index.html b/v1/sp/docs/profiles/index.html new file mode 100644 index 000000000..bb8c1fc30 --- /dev/null +++ b/v1/sp/docs/profiles/index.html @@ -0,0 +1,1794 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Profiles - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/profiles

+

The profile services allows to to work with the SharePoint User Profile Store.

+

Profiles

+

Profiles is accessed directly from the root sp object.

+
import { sp } from "@pnp/sp";
+
+ + +

GET

+

Get profile properties for a specific user

+

getPropertiesFor(loginName: string): Promise<any>;

+
sp
+  .profiles
+  .getPropertiesFor(loginName).then((profile: any) => {
+
+    console.log(profile.DisplayName);
+    console.log(profile.Email);
+    console.log(profile.Title);
+    console.log(profile.UserProfileProperties.length);
+
+    // Properties are stored in inconvenient Key/Value pairs,
+    // so parse into an object called userProperties
+    var properties = {};
+    profile.UserProfileProperties.forEach(function(prop) {
+    properties[prop.Key] = prop.Value;
+    });
+    profile.userProperties = properties;
+
+}
+
+ + +

Get a specific property for a specific user

+

getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>;

+
sp
+  .profiles
+  .getUserProfilePropertyFor(loginName, propName).then((prop: string) => {
+    console.log(prop);
+};
+
+ + +

Find whether a user is following another user

+

isFollowing(follower: string, followee: string): Promise<boolean>;

+
sp
+  .profiles
+  .isFollowing(follower, followee).then((followed: boolean) => {
+    console.log(followed);
+};
+
+ + +

Find out who a user is following

+

getPeopleFollowedBy(loginName: string): Promise<any[]>;

+
sp
+  .profiles
+  .getPeopleFollowedBy(loginName).then((followed: any[]) => {
+    console.log(followed.length);
+};
+
+ + +

Find out if the current user is followed by another user

+

amIFollowedBy(loginName: string): Promise<boolean>;

+

Returns a boolean indicating if the current user is followed by the user with loginName. +Get a specific property for the specified user.

+
sp
+  .profiles
+  .amIFollowedBy(loginName).then((followed: boolean) => {
+    console.log(followed);
+};
+
+ + +

Get the people who are following the specified user

+

getFollowersFor(loginName: string): Promise<any[]>;

+
sp
+  .profiles
+  .getFollowersFor(loginName).then((followed: any) => {
+    console.log(followed.length);
+};
+
+ + +

SET

+

Set a single value property value

+

setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string)

+

Set a user's user profile property.

+
sp
+  .profiles
+  .setSingleValueProfileProperty(accountName, propertyName, propertyValue);
+
+ + +

Set multi valued User Profile property

+

setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>;

+
sp
+  .profiles
+  .setSingleValueProfileProperty(accountName, propertyName, propertyValues);
+
+ + +

Upload and set the user profile picture

+

Users can upload a picture to their own profile only). Not supported for batching. +Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB

+

setMyProfilePic(profilePicSource: Blob): Promise<void>;

+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/related-items/index.html b/v1/sp/docs/related-items/index.html new file mode 100644 index 000000000..9ae3ab2ce --- /dev/null +++ b/v1/sp/docs/related-items/index.html @@ -0,0 +1,1886 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Related Items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/relateditems

+

Related items are used in Task and Workflow lists (as well as others) to track items that have relationships similar to database relationships.

+

All methods chain off the Web's relatedItems property as shown below:

+

getRelatedItems

+

Expects the named library to exist within the contextual web.

+
import { sp, RelatedItem } from "@pnp/sp";
+
+sp.web.relatedItems.getRelatedItems("Documents", 1).then((result: RelatedItem[]) => {
+
+    console.log(result);
+});
+
+ + +

getPageOneRelatedItems

+

Expects the named library to exist within the contextual web.

+
import { sp, RelatedItem } from "@pnp/sp";
+
+sp.web.relatedItems.getPageOneRelatedItems("Documents", 1).then((result: RelatedItem[]) => {
+
+    console.log(result);
+});
+
+ + + +
import { sp } from "@pnp/sp";
+
+sp.web.relatedItems.addSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev").then(_ => {
+
+    // ... return is void
+});
+
+sp.web.relatedItems.addSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev", true).then(_ => {
+
+    // ... return is void
+});
+
+ + +

addSingleLinkToUrl

+

Adds a related item link from an item specified by list name and item id, to an item specified by url

+
import { sp } from "@pnp/sp";
+
+sp.web.relatedItems.addSingleLinkToUrl("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt").then(_ => {
+
+    // ... return is void
+});
+
+sp.web.relatedItems.addSingleLinkToUrl("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", true).then(_ => {
+    // ... return is void
+});
+
+ + +

addSingleLinkFromUrl

+

Adds a related item link from an item specified by url, to an item specified by list name and item id

+
import { sp } from "@pnp/sp";
+
+sp.web.relatedItems.addSingleLinkFromUrl("https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", "RelatedItemsList1", 2).then(_ => {
+    // ... return is void
+});
+
+sp.web.relatedItems.addSingleLinkFromUrl("https://site.sharepoint.com/sites/dev/subsite/Documents/test.txt", "RelatedItemsList1", 2, true).then(_ => {
+
+    // ... return is void
+});
+
+ + + +
import { sp } from "@pnp/sp";
+
+sp.web.relatedItems.deleteSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev").then(_ => {
+
+    // ... return is void
+});
+
+sp.web.relatedItems.deleteSingleLink("RelatedItemsList1", 2, "https://site.sharepoint.com/sites/dev/subsite", "RelatedItemsList2", 1, "https://site.sharepoint.com/sites/dev", true).then(_ => {
+
+    // ... return is void
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/search/index.html b/v1/sp/docs/search/index.html new file mode 100644 index 000000000..0a8f29fbe --- /dev/null +++ b/v1/sp/docs/search/index.html @@ -0,0 +1,1830 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Search - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/search

+

Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and search suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.

+

Search

+

Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the SearchQuery interface, or a SearchQueryBuilder instance. The first two are shown below.

+
import { sp, SearchQuery, SearchResults } from "@pnp/sp";
+
+// text search using SharePoint default values for other parameters
+sp.search("test").then((r: SearchResults) => {
+
+    console.log(r.ElapsedTime);
+    console.log(r.RowCount);
+    console.log(r.PrimarySearchResults);
+});
+
+// define a search query object matching the SearchQuery interface
+sp.search(<SearchQuery>{
+    Querytext: "test",
+    RowLimit: 10,
+    EnableInterleaving: true,
+}).then((r: SearchResults) => {
+
+    console.log(r.ElapsedTime);
+    console.log(r.RowCount);
+    console.log(r.PrimarySearchResults);
+});
+
+ + +

Search Result Caching

+

Added in 1.1.5

+

As of version 1.1.5 you can also use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace "search" with "searchWithCaching" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options

+
import { sp, SearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp";
+
+sp.searchWithCaching(<SearchQuery>{
+    Querytext: "test",
+    RowLimit: 10,
+    EnableInterleaving: true,
+}).then((r: SearchResults) => {
+
+    console.log(r.ElapsedTime);
+    console.log(r.RowCount);
+    console.log(r.PrimarySearchResults);
+});
+
+
+const builder = SearchQueryBuilder().text("test").rowLimit(3);
+
+// supply a search query builder and caching options
+sp.searchWithCaching(builder, { key: "mykey", expiration: dateAdd(new Date(), "month", 1) }).then(r2 => {
+
+    console.log(r2.TotalRows);
+});
+
+ + +

Paging with SearchResults.getPage

+

Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.

+
import { sp, SearchQueryBuilder, SearchResults } from "@pnp/sp";
+
+// this will hold our current results
+let currentResults: SearchResults = null;
+let page = 1;
+
+// triggered on page load through some means
+function onStart() {
+
+    // construct our query that will be throughout the paging process, likely from user input
+    const q = SearchQueryBuilder.create("test").rowLimit(5);
+    sp.search(q).then((r: SearchResults) => {
+
+        currentResults = r; // update the current results
+        page = 1; // reset if needed
+        // update UI with data...
+    });
+}
+
+// triggered by an event
+function next() {
+    currentResults.getPage(++page).then((r: SearchResults) => {
+
+        currentResults = r; // update the current results
+        // update UI with data...
+    });
+}
+
+// triggered by an event
+function prev() {
+    currentResults.getPage(--page).then((r: SearchResults) => {
+
+        currentResults = r; // update the current results
+        // update UI with data...
+    });
+}
+
+ + +

SearchQueryBuilder

+

The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.

+
import { SearchQueryBuilder } from "@pnp/sp";
+
+// basic usage
+let q = SearchQueryBuilder().text("test").rowLimit(4).enablePhonetic;
+
+sp.search(q).then(h => { /* ... */ });
+
+// provide a default query text in the create()
+let q2 = SearchQueryBuilder("text").rowLimit(4).enablePhonetic;
+
+sp.search(q2).then(h => { /* ... */ });
+
+// provide query text and a template
+
+// shared settings across queries
+const appSearchSettings: SearchQuery = {
+    EnablePhonetic: true,
+    HiddenConstraints: "reports"
+};
+
+let q3 = SearchQueryBuilder("test", appSearchSettings).enableQueryRules;
+let q4 = SearchQueryBuilder("financial data", appSearchSettings).enableSorting.enableStemming;
+sp.search(q3).then(h => { /* ... */ });
+sp.search(q4).then(h => { /* ... */ });
+
+ + +

Search Suggest

+

Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches SearchSuggestQuery.

+
import { sp, SearchSuggestQuery, SearchSuggestResult } from "@pnp/sp";
+
+sp.searchSuggest("test").then((r: SearchSuggestResult) => {
+
+    console.log(r);
+});
+
+sp.searchSuggest(<SearchSuggestQuery>{
+    querytext: "test",
+    count: 5,
+}).then((r: SearchSuggestResult) => {
+
+    console.log(r);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/sharing/index.html b/v1/sp/docs/sharing/index.html new file mode 100644 index 000000000..ed1915e02 --- /dev/null +++ b/v1/sp/docs/sharing/index.html @@ -0,0 +1,2058 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sharing - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/sp/sharing

+

Note: This API is still considered "beta" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.

+

One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before submitting an issue.

+ +

Applies to: Item, Folder, File

+

Creates a sharing link for the given resource with an optional expiration.

+
import { sp , SharingLinkKind, ShareLinkResponse } from "@pnp/sp";
+import { dateAdd } from "@pnp/core";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView).then(((result: ShareLinkResponse) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), "day", 5)).then((result: ShareLinkResponse) => {
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

shareWith

+

Applies to: Item, Folder, File, Web

+

Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter "shareEverything" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.

+
import { sp , SharingResult, SharingRole } from "@pnp/sp";
+
+sp.web.shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit).then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit, true, true).then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com").then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit).then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

shareObject & shareObjectRaw

+

Applies to: Web

+

Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.

+
import { sp , SharingResult, SharingRole } from "@pnp/sp";
+
+sp.web.shareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt", "i:0#.f|membership|user@site.com", SharingRole.View).then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.shareObjectRaw({
+    url: "https://mysite.sharepoint.com/sites/dev/Docs/test.txt",
+    peoplePickerInput: [{ Key: "i:0#.f|membership|user@site.com" }],
+    roleValue: "role: 1973741327",
+    groupId: 0,
+    propagateAcl: false,
+    sendEmail: true,
+    includeAnonymousLinkInEmail: false,
+    emailSubject: "subject",
+    emailBody: "body",
+    useSimplifiedRoles: true,
+});
+
+ + +

unshareObject

+

Applies to: Web

+
import { sp , SharingResult } from "@pnp/sp";
+
+sp.web.unshareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt").then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

checkSharingPermissions

+

Applies to: Item, Folder, File

+

Checks Permissions on the list of Users and returns back role the users have on the Item.

+
import { sp , SharingEntityPermission } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").checkSharingPermissions([{ alias: "i:0#.f|membership|user@site.com" }]).then((result: SharingEntityPermission[]) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

getSharingInformation

+

Applies to: Item, Folder, File

+

Get Sharing Information.

+
import { sp , SharingInformation } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation().then((result: SharingInformation) => {
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

getObjectSharingSettings

+

Applies to: Item, Folder, File

+

Gets the sharing settings

+
import { sp , ObjectSharingSettings } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getObjectSharingSettings().then((result: ObjectSharingSettings) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

unshare

+

Applies to: Item, Folder, File

+

Unshares a given resource

+
import { sp , SharingResult } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshare().then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + +

deleteSharingLinkByKind

+

Applies to: Item, Folder, File

+
import { sp , SharingLinkKind, SharingResult } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit).then((result: SharingResult) => {
+
+    console.log(result);
+}).catch(e => {
+    console.error(e);
+});
+
+ + + +

Applies to: Item, Folder, File

+
import { sp , SharingLinkKind } from "@pnp/sp";
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit).then(_ => {
+
+    console.log("done");
+}).catch(e => {
+    console.error(e);
+});
+
+sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit, "12345").then(_ => {
+
+    console.log("done");
+}).catch(e => {
+    console.error(e);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/sitedesigns/index.html b/v1/sp/docs/sitedesigns/index.html new file mode 100644 index 000000000..c2b2d9132 --- /dev/null +++ b/v1/sp/docs/sitedesigns/index.html @@ -0,0 +1,1851 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Site Designs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/sitedesigns

+

You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. +Check out SharePoint site design and site script overview for more information.

+

Site Designs

+

Create a new site design

+
import { sp } from "@pnp/sp";
+
+// WebTemplate: 64 Team site template, 68 Communication site template
+const siteDesign = await sp.siteDesigns.createSiteDesign({
+    SiteScriptIds: ["884ed56b-1aab-4653-95cf-4be0bfa5ef0a"],
+    Title: "SiteDesign001",
+    WebTemplate: "64",
+});
+
+console.log(siteDesign.Title);
+
+ + +

Applying a site design to a site

+
import { sp } from "@pnp/sp";
+
+// Limited to 30 actions in a site script, but runs synchronously
+await sp.siteDesigns.applySiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8","https://contoso.sharepoint.com/sites/teamsite-pnpjs001");
+
+// Better use the following method for 300 actions in a site script
+const task = await sp.web.addSiteDesignTask("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+ + +

Retrieval

+
import { sp } from "@pnp/sp";
+
+// Retrieving all site designs
+const allSiteDesigns = await sp.siteDesigns.getSiteDesigns();
+console.log(`Total site designs: ${allSiteDesigns.length}`);
+
+// Retrieving a single site design by Id
+const siteDesign = await sp.siteDesigns.getSiteDesignMetadata("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+console.log(siteDesign.Title);
+
+ + +

Update and delete

+
import { sp } from "@pnp/sp";
+
+// Update
+const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: "75b9d8fe-4381-45d9-88c6-b03f483ae6a8", Title: "SiteDesignUpdatedTitle001" });
+
+// Delete
+await sp.siteDesigns.deleteSiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+ + +

Setting Rights/Permissions

+
import { sp } from "@pnp/sp";
+
+// Get
+const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+console.log(rights.length > 0 ? rights[0].PrincipalName : "");
+
+// Grant
+await sp.siteDesigns.grantSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
+
+// Revoke
+await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
+
+// Reset all view rights
+const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", rights.map(u => u.PrincipalName));
+
+ + +

Get a history of site designs that have run on a web

+
import { sp } from "@pnp/sp";
+
+const runs = await sp.web.getSiteDesignRuns();
+const runs2 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite");
+
+// Get runs specific to a site design
+const runs3 = await sp.web.getSiteDesignRuns("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+const runs4 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite", "75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
+
+// For more information about the site script actions
+const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID);
+const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus("https://TENANT.sharepoint.com/sites/mysite", runs[0].ID);
+
+ + +

Site Scripts

+

Create a new site script

+
import { sp } from "@pnp/sp";
+
+const sitescriptContent = {
+    "$schema": "schema.json",
+    "actions": [
+        {
+            "themeName": "Theme Name 123",
+            "verb": "applyTheme",
+        },
+    ],
+    "bindata": {},
+    "version": 1,
+};
+
+const siteScript = await sp.siteScripts.createSiteScript("Title", "description", sitescriptContent);
+
+console.log(siteScript.Title);
+
+ + +

Retrieval

+
import { sp } from "@pnp/sp";
+
+// Retrieving all site scripts
+const allSiteScripts = await sp.siteScripts.getSiteScripts();
+console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : "");
+
+// Retrieving a single site script by Id
+const siteScript = await sp.siteScripts.getSiteScriptMetadata("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
+console.log(siteScript.Title);
+
+ + +

Update and delete

+
import { sp } from "@pnp/sp";
+
+// Update
+const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: "884ed56b-1aab-4653-95cf-4be0bfa5ef0a", Title: "New Title" });
+console.log(updatedSiteScript.Title);
+
+// Delete
+await sp.siteScripts.deleteSiteScript("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
+
+ + +

Get site script from a list

+
import { sp } from "@pnp/sp";
+
+// Using the absolute URL of the list
+const ss = await sp.siteScripts.getSiteScriptFromList("https://TENANT.sharepoint.com/Lists/mylist");
+
+// Using the PnPjs web object to fetch the site script from a specific list
+const ss2 = await sp.web.lists.getByTitle("mylist").getSiteScript();
+
+ + +

Get site script from a web

+
import { sp } from "@pnp/sp";
+
+const extractInfo = {
+    IncludeBranding: true,
+    IncludeLinksToExportedItems: true,
+    IncludeRegionalSettings: true,
+    IncludeSiteExternalSharingCapability: true,
+    IncludeTheme: true,
+    IncludedLists: ["Lists/MyList"]
+};
+
+const ss = await sp.siteScripts.getSiteScriptFromWeb("https://TENANT.sharepoint.com/sites/mysite", extractInfo);
+
+// Using the PnPjs web object to fetch the site script from a specific web
+const ss2 = await sp.web.getSiteScript(extractInfo);
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/sites/index.html b/v1/sp/docs/sites/index.html new file mode 100644 index 000000000..863050ded --- /dev/null +++ b/v1/sp/docs/sites/index.html @@ -0,0 +1,2186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/sp/site - Site properties

+

Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.

+

Get context information for the current site collection

+

Using the library, you can get the context information of the current site collection

+
import { sp } from "@pnp/sp";
+
+sp.site.getContextInfo().then(d =>{
+       console.log(d.FormDigestValue); 
+});
+
+ + +

Get document libraries of a web

+

Using the library, you can get a list of the document libraries present in the a given web.

+

Note: Works only in SharePoint online

+
import { sp } from "@pnp/sp";
+
+sp.site.getDocumentLibraries("https://tenant.sharepoint.com/sites/test/subsite").then((d:DocumentLibraryInformation[]) => {
+    // iterate over the array of doc lib
+});
+
+ + +

Open Web By Id

+

Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.

+
sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e").then(w => {
+
+    //we got all the data from the web as well
+    console.log(w.data);
+
+    // we can chain
+    w.web.select("Title").get().then(w2 => {
+        // ...
+    });
+});
+
+ + +

Get site collection url from page

+

Using the library, you can get the site collection url by providing a page url

+
import { sp } from "@pnp/sp";
+
+sp.site.getWebUrlFromPageUrl("https://tenant.sharepoint.com/sites/test/Pages/test.aspx").then(d => {
+        console.log(d);
+});
+
+ + +

Join a hub site

+

Added in 1.2.4

+

Note: Works only in SharePoint online

+

Join the current site collection to a hub site collection

+
import { sp, Site } from "@pnp/sp";
+
+var site = new Site("https://tenant.sharepoint.com/sites/HubSite/");
+
+var hubSiteID = "";
+
+site.select("ID").get().then(d => {
+    // get ID of the hub site collection
+    hubSiteID = d.Id;
+
+    // associate the current site collection the hub site collection
+    sp.site.joinHubSite(hubSiteID).then(d => {
+        console.log(d);
+    });
+
+});
+
+ + +

Disassociate the current site collection from a hub site collection

+

Added in 1.2.4

+

Note: Works only in SharePoint online

+
import { sp } from "@pnp/sp";
+
+sp.site.joinHubSite("00000000-0000-0000-0000-000000000000").then(d => {
+    console.log(d);
+});
+
+ + +

Register a hub site

+

Added in 1.2.4

+

Note: Works only in SharePoint online

+

Registers the current site collection as a hub site collection

+
import { sp } from "@pnp/sp";
+
+sp.site.registerHubSite().then(d => {
+    console.log(d);
+});
+
+ + +

Un-Register a hub site

+

Added in 1.2.4

+

Note: Works only in SharePoint online

+

Un-Registers the current site collection as a hub site collection

+
import { sp } from "@pnp/sp";
+
+sp.site.unRegisterHubSite().then(d => {
+    console.log(d);
+});
+
+ + +

Create a modern communication site

+

Added in 1.2.6

+

Note: Works only in SharePoint online

+

Creates a modern communication site.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeRequiredDescription
TitlestringyesThe title of the site to create.
lcidnumberyesThe default language to use for the site.
shareByEmailEnabledbooleanyesIf set to true, it will enable sharing files via Email. By default it is set to false
urlstringyesThe fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site.
descriptionstringnoThe description of the communication site.
classificationstringnoThe Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information
siteDesignIdstringnoThe Guid of the site design to be used.
You can use the below default OOTB GUIDs:
Topic: null
Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767
Blank: f6cc5403-0d63-442e-96c0-285923709ffc
hubSiteIdstringnoThe Guid of the already existing Hub site
ownerstringnoRequired when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com
+
import { sp } from "@pnp/sp";
+
+const s = await sp.site.createCommunicationSite(
+            "Title",
+            1033,
+            true,
+            "https://tenant.sharepoint.com/sites/commSite",
+            "Description",
+            "HBI",
+            "f6cc5403-0d63-442e-96c0-285923709ffc",
+            "a00ec589-ea9f-4dba-a34e-67e78d41e509",
+            "user@TENANT.onmicrosoft.com");
+
+ + +

Create a modern team site

+

Added in 1.2.6

+

Note: Works only in SharePoint online. It wont work with App only tokens

+

Creates a modern team site backed by O365 group.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PropertyTypeRequiredDescription
displayNamestringyesThe title/displayName of the site to be created.
aliasstringyesAlias of the underlying Office 365 Group.
isPublicbooleanyesDefines whether the Office 365 Group will be public (default), or private.
lcidnumberyesThe language to use for the site. If not specified will default to English (1033).
descriptionstringnoThe description of the modern team site.
classificationstringnoThe Site classification to use. For instance 'Contoso Classified'. See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information
ownersstring array (string[])noThe Owners of the site to be created
hubSiteIdstringnoThe Guid of the already existing Hub site
+
import { sp } from "@pnp/sp";
+
+sp.site.createModernTeamSite(
+        "displayName",
+        "alias",
+        true,
+        1033,
+        "description",
+        "HBI",
+        ["user1@tenant.onmicrosoft.com","user2@tenant.onmicrosoft.com","user3@tenant.onmicrosoft.com"],
+        "a00ec589-ea9f-4dba-a34e-67e78d41e509")
+        .then(d => {
+            console.log(d);
+        });
+
+ + +

Delete a site collection

+
import { sp } from "@pnp/sp";
+
+// Delete the current site
+await sp.site.delete();
+
+// Specify which site to delete
+const siteUrl = "https://tenant.sharepoint.com/sites/tstpnpsitecoldelete5";
+const site2 = new Site(siteUrl);
+await site2.delete();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/social/index.html b/v1/sp/docs/social/index.html new file mode 100644 index 000000000..eaf0886cf --- /dev/null +++ b/v1/sp/docs/social/index.html @@ -0,0 +1,2011 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Social - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/social

+

The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not +with app-only permissions.

+

getFollowedSitesUri

+

Gets a URI to a site that lists the current user's followed sites.

+
import { sp } from "@pnp/sp";
+
+const uri = await sp.social.getFollowedSitesUri();
+
+ + +

getFollowedDocumentsUri

+

Gets a URI to a site that lists the current user's followed documents.

+
import { sp } from "@pnp/sp";
+
+const uri = await sp.social.getFollowedDocumentsUri();
+
+ + +

follow

+

Makes the current user start following a user, document, site, or tag

+
import { sp, SocialActorType } from "@pnp/sp";
+
+// follow a site
+const r1 = await sp.social.follow({
+    ActorType: SocialActorType.Site,
+    ContentUri: "htts://tenant.sharepoint.com/sites/site",
+});
+
+// follow a person
+const r2 = await sp.social.follow({
+    AccountName: "i:0#.f|membership|person@tenant.com",
+    ActorType: SocialActorType.User,
+});
+
+// follow a doc
+const r3 = await sp.social.follow({
+    ActorType: SocialActorType.Document,
+    ContentUri: "https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx",
+});
+
+// follow a tag
+// You need the tag GUID to start following a tag.
+// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model.
+// See How to get a tag's GUID based on the tag's name by using the JavaScript object model.
+// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid
+const r4 = await sp.social.follow({
+    ActorType: SocialActorType.Tag,
+    TagGuid: "19a4a484-c1dc-4bc5-8c93-bb96245ce928",
+});
+
+ + +

isFollowed

+

Indicates whether the current user is following a specified user, document, site, or tag

+
import { sp, SocialActorType } from "@pnp/sp";
+
+// pass the same social actor struct as shown in follow example for each type
+const r = await sp.social.isFollowed({
+    AccountName: "i:0#.f|membership|person@tenant.com",
+    ActorType: SocialActorType.User,
+});
+
+ + +

stopFollowing

+

Makes the current user stop following a user, document, site, or tag

+
import { sp, SocialActorType } from "@pnp/sp";
+
+// pass the same social actor struct as shown in follow example for each type
+const r = await sp.social.stopFollowing({
+    AccountName: "i:0#.f|membership|person@tenant.com",
+    ActorType: SocialActorType.User,
+});
+
+ + +

my

+

get

+

Gets this user's social information

+
import { sp } from "@pnp/sp";
+
+const r = await sp.social.my.get();
+
+ + +

followed

+

Gets users, documents, sites, and tags that the current user is following based on the supplied flags.

+
import { sp, SocialActorTypes } from "@pnp/sp";
+
+// get all the followed documents
+const r1 = await sp.social.my.followed(SocialActorTypes.Document);
+
+// get all the followed documents and sites
+const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site);
+
+// get all the followed sites updated in the last 24 hours
+const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);
+
+ + +

followedCount

+

Works as followed but returns on the count of actors specifed by the query

+
import { sp, SocialActorTypes } from "@pnp/sp";
+
+// get the followed documents count
+const r = await sp.social.my.followedCount(SocialActorTypes.Document);
+
+ + +

followers

+

Gets the users who are following the current user.

+
import { sp } from "@pnp/sp";
+
+// get the followed documents count
+const r = await sp.social.my.followers();
+
+ + +

suggestions

+

Gets users who the current user might want to follow.

+
import { sp } from "@pnp/sp";
+
+// get the followed documents count
+const r = await sp.social.my.suggestions();
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/sp-utilities-utility/index.html b/v1/sp/docs/sp-utilities-utility/index.html new file mode 100644 index 000000000..e3d52def5 --- /dev/null +++ b/v1/sp/docs/sp-utilities-utility/index.html @@ -0,0 +1,2061 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SP.Utilities.Utility - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + + + + +
+
+ + + + + +

@pnp/sp/utilities

+

Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.

+

sendEmail

+

This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).

+

EmailProperties

+
export interface EmailProperties {
+
+    To: string[];
+    CC?: string[];
+    BCC?: string[];
+    Subject: string;
+    Body: string;
+    AdditionalHeaders?: TypedHash<string>;
+    From?: string;
+}
+
+ + +

Usage

+

You must define the To, Subject, and Body values - the remaining are optional.

+
import { sp, EmailProperties } from "@pnp/sp";
+
+const emailProps: EmailProperties = {
+    To: ["user@site.com"],
+    CC: ["user2@site.com", "user3@site.com"],
+    Subject: "This email is about...",
+    Body: "Here is the body. <b>It supports html</b>",
+};
+
+sp.utility.sendEmail(emailProps).then(_ => {
+
+    console.log("Email Sent!");
+});
+
+ + +

getCurrentUserEmailAddresses

+

This method returns the current user's email addresses known to SharePoint.

+
import { sp } from "@pnp/sp";
+
+sp.utility.getCurrentUserEmailAddresses().then((addressString: string) => {
+
+    console.log(addressString);
+});
+
+ + +

resolvePrincipal

+

Gets information about a principal that matches the specified Search criteria

+
import { sp , PrincipalType, PrincipalSource, PrincipalInfo } from "@pnp/sp";
+
+sp.utility.resolvePrincipal("user@site.com",
+    PrincipalType.User,
+    PrincipalSource.All,
+    true,
+    false).then((principal: PrincipalInfo) => {
+
+
+        console.log(principal);
+    });
+
+ + +

searchPrincipals

+

Gets information about the principals that match the specified Search criteria.

+
import { sp , PrincipalType, PrincipalSource, PrincipalInfo } from "@pnp/sp";
+
+sp.utility.searchPrincipals("john",
+    PrincipalType.User,
+    PrincipalSource.All,
+    "",
+    10).then((principals: PrincipalInfo[]) => {
+
+        console.log(principals);
+    });
+
+ + +

createEmailBodyForInvitation

+

Gets the external (outside the firewall) URL to a document or resource in a site.

+
import { sp } from "@pnp/sp";
+
+sp.utility.createEmailBodyForInvitation("https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx").then((r: string) => {
+
+    console.log(r);
+});
+
+ + +

expandGroupsToPrincipals

+

Resolves the principals contained within the supplied groups

+
import { sp , PrincipalInfo } from "@pnp/sp";
+
+sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"]).then((principals: PrincipalInfo[]) => {
+
+    console.log(principals);
+});
+
+// optionally supply a max results count. Default is 30.
+sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"], 10).then((principals: PrincipalInfo[]) => {
+
+    console.log(principals);
+});
+
+ + +

createWikiPage

+
import { sp , CreateWikiPageResult } from "@pnp/sp";
+
+sp.utility.createWikiPage({
+    ServerRelativeUrl: "/sites/dev/SitePages/mynewpage.aspx",
+    WikiHtmlContent: "This is my <b>page</b> content. It supports rich html.",
+}).then((result: CreateWikiPageResult) => {
+
+    // result contains the raw data returned by the service
+    console.log(result.data);
+
+    // result contains a File instance you can use to further update the new page
+    result.file.get().then(f => {
+
+        console.log(f);
+    });
+});
+
+ + +

containsInvalidFileFolderChars

+

Checks if file or folder name contains invalid characters

+
import { sp } from "@pnp/sp";
+
+const isInvalid = sp.utility.containsInvalidFileFolderChars("Filename?.txt");
+console.log(isInvalid); // true
+
+ + +

stripInvalidFileFolderChars

+

Removes invalid characters from file or folder name

+
import { sp } from "@pnp/sp";
+
+const validName = sp.utility.stripInvalidFileFolderChars("Filename?.txt");
+console.log(validName); // Filename.txt
+
+ + +

Call Other Methods

+

Even if a method does not have an explicit implementation on the utility api you can still call it using the UtilityMethod class. In this example we will show calling the GetLowerCaseString method, but the technique works for any of the utility methods.

+
import { UtilityMethod } from "@pnp/sp";
+
+// the first parameter is the web url. You can use an empty string for the current web,
+// or specify it to call other web's. The second parameter is the method name.
+const method = new UtilityMethod("", "GetLowerCaseString");
+
+// you must supply the correctly formatted parameters to the execute method which
+// is generic and types the result as the supplied generic type parameter.
+method.excute<string>({
+    sourceValue: "HeRe IS my StrINg",
+    lcid: 1033,
+}).then((s: string) => {
+
+    console.log(s);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/tenant-properties/index.html b/v1/sp/docs/tenant-properties/index.html new file mode 100644 index 000000000..b16248f46 --- /dev/null +++ b/v1/sp/docs/tenant-properties/index.html @@ -0,0 +1,1795 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Tenant Properties - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/web - tenant properties

+

You can set, read, and remove tenant properties using the methods shown below:

+

setStorageEntity

+

This method MUST be called in the context of the app catalog web or you will get an access denied message.

+
import { Web } from "@pnp/sp";
+
+const w = new Web("https://tenant.sharepoint.com/sites/appcatalog/");
+
+// specify required key and value
+await w.setStorageEntity("Test1", "Value 1");
+
+// specify optional description and comments
+await w.setStorageEntity("Test2", "Value 2", "description", "comments");
+
+ + +

getStorageEntity

+

This method can be used from any web to retrieve values previsouly set.

+
import { sp, StorageEntity } from "@pnp/sp";
+
+const prop: StorageEntity = await sp.web.getStorageEntity("Test1");
+
+console.log(prop.Value);
+
+ + +

removeStorageEntity

+

This method MUST be called in the context of the app catalog web or you will get an access denied message.

+
import { Web } from "@pnp/sp";
+
+const w = new Web("https://tenant.sharepoint.com/sites/appcatalog/");
+
+await w.removeStorageEntity("Test1");
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/views/index.html b/v1/sp/docs/views/index.html new file mode 100644 index 000000000..3d8bdc89b --- /dev/null +++ b/v1/sp/docs/views/index.html @@ -0,0 +1,1869 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Views - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + +

@pnp/sp/views

+

Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.

+

Get a View's Properties

+

To get a views properties you need to know it's id or title. You can use the standard OData operators as expected to select properties. For a list of the properties, please see this article.

+
import { sp } from "@pnp/sp";
+// know a view's GUID id
+sp.web.lists.getByTitle("Documents").getView("2B382C69-DF64-49C4-85F1-70FB9CECACFE").select("Title").get().then(v => {
+
+    console.log(v);
+});
+
+// get by the display title of the view
+sp.web.lists.getByTitle("Documents").views.getByTitle("All Documents").select("Title").get().then(v => {
+
+    console.log(v);
+});
+
+ + +

Add a View

+

To add a view you use the add method of the views collection. You must supply a title and can supply other parameters as well.

+
import { sp, ViewAddResult } from "@pnp/sp";
+// create a new view with default fields and properties
+sp.web.lists.getByTitle("Documents").views.add("My New View").then(v => {
+
+    console.log(v);
+});
+
+// create a new view with specific properties
+sp.web.lists.getByTitle("Documents").views.add("My New View 2", false, {
+
+    RowLimit: 10,
+    ViewQuery: "<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>",
+}).then((v: ViewAddResult) => {
+
+    // manipulate the view's fields
+    v.view.fields.removeAll().then(_ => {
+
+        Promise.all([
+            v.view.fields.add("Title"),
+            v.view.fields.add("Modified"),
+        ]).then(_ =>{
+
+            console.log("View created");
+        });
+    });
+});
+
+ + +

Update a View

+
import { sp, ViewUpdateResult } from "@pnp/sp";
+
+sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").update({
+    RowLimit: 20,
+}).then((v: ViewUpdateResult) => {
+
+    console.log(v);
+});
+
+ + +

Set View XML

+

Added in 1.2.6

+
import { sp } from "@pnp/sp";
+
+const viewXml: string = "...";
+
+await sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").setViewXml(viewXml);
+
+ + +

Delete a View

+
import { sp } from "@pnp/sp";
+
+sp.web.lists.getByTitle("Documents").views.getByTitle("My New View").delete().then(_ => {
+
+    console.log("View deleted");
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v1/sp/docs/webs/index.html b/v1/sp/docs/webs/index.html new file mode 100644 index 000000000..95d106c26 --- /dev/null +++ b/v1/sp/docs/webs/index.html @@ -0,0 +1,1972 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Webs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Skip to content + + + +
+ +
+ +
+ + + + + + + + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+ +
+ + +
+
+ + + + + +

@pnp/sp/webs

+

Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.

+

Add a Web

+

Using the library you can add a web to another web's collection of subwebs. The basic usage requires only a title and url. This will result in a team site with all of the default settings.

+
import { sp, WebAddResult } from "@pnp/sp";
+
+sp.web.webs.add("title", "subweb1").then((w: WebAddResult) => {
+
+    // show the response from the server when adding the web
+    console.log(w.data);
+
+    w.web.select("Title").get().then(w => {
+
+        // show our title
+        console.log(w.Title);
+    });
+});
+
+ + +

You can also provide other settings such as description, template, language, and inherit permissions.

+
import { sp, WebAddResult } from "@pnp/sp";
+
+// create a German language wiki site with title, url, description, which inherits permissions
+sp.web.webs.add("wiki", "subweb2", "a wiki web", "WIKI#0", 1031, true).then((w: WebAddResult) => {
+
+    // show the response from the server when adding the web
+    console.log(w.data);
+
+    w.web.select("Title").get().then(w => {
+
+        // show our title
+        console.log(w.Title);
+    });
+});
+
+ + +

Create Default Associated Groups

+

If you create a web that doesn't inherit permissions from the parent web, you can create its default associated groups (Members, Owners, Visitors) with the default role assigments (Contribute, Full Control, Read)

+
import { sp, WebAddResult } from "@pnp/sp";
+
+sp.web.webs.add("title", "subweb1", "a wiki web", "WIKI#0", 1031, false).then((w: WebAddResult) => {
+
+    w.web.createDefaultAssociatedGroups().then(() => {
+
+        // ...
+    });
+});
+
+ + +

Get A Web's properties

+
import { sp } from "@pnp/sp";
+
+// basic get of the webs properties
+sp.web.get().then(w => {
+
+    console.log(w.Title);
+});
+
+// use odata operators to get specific fields
+sp.web.select("Title").get().then(w => {
+
+    console.log(w.Title);
+});
+
+// use with get to give the result a type
+sp.web.select("Title").get<{ Title: string }>().then(w => {
+
+    console.log(w.Title);
+});
+
+ + +

Get Complex Properties

+

Some properties, such as AllProperties, are not returned by default. You can still access them using the expand operator.

+
import { sp } from "@pnp/sp";
+
+sp.web.select("AllProperties").expand("AllProperties").get().then(w => {
+
+    console.log(w.AllProperties);
+});
+
+ + +

Get a Web Directly

+

You can also use the Web object directly to get any web, though of course the current user must have the necessary permissions. This is done by importing the web object.

+
import { Web } from "@pnp/sp";
+
+let web = new Web("https://my-tenant.sharepoint.com/sites/mysite");
+
+web.get().then(w => {
+
+    console.log(w);
+});
+
+ + +

Open Web By Id

+

Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.

+
sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e").then(w => {
+
+    //we got all the data from the web as well
+    console.log(w.data);
+
+    // we can chain
+    w.web.select("Title").get().then(w2 => {
+        // ...
+    });
+});
+
+ + +

Update Web Properties

+

You can update web properties using the update method. The properties available for update are listed in this table. Updating is a simple as passing a plain object with the properties you want to update.

+
import { Web } from "@pnp/sp";
+
+let web = new Web("https://my-tenant.sharepoint.com/sites/mysite");
+
+web.update({
+    Title: "New Title",
+    CustomMasterUrl: "{path to masterpage}",
+    Description: "My new description",
+}).then(w => {
+
+    console.log(w);
+});
+
+ + +

Delete a Web

+
import { Web } from "@pnp/sp";
+
+let web = new Web("https://my-tenant.sharepoint.com/sites/mysite");
+
+web.delete().then(w => {
+
+    console.log(w);
+});
+
+ + + + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + spacer + + + \ No newline at end of file diff --git a/v2/404.html b/v2/404.html new file mode 100644 index 000000000..c147c3f0f --- /dev/null +++ b/v2/404.html @@ -0,0 +1,2108 @@ + + + + + + + + + + + + + + + + PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ +
+ + + + +
+
+ + +
+
+
+ +
+
+
+ + + +
+
+ +

404 - Not found

+ + + + + + +
+
+
+
+ + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/v2/SPFx-on-premises/index.html b/v2/SPFx-on-premises/index.html new file mode 100644 index 000000000..3b65b82c4 --- /dev/null +++ b/v2/SPFx-on-premises/index.html @@ -0,0 +1,2278 @@ + + + + + + + + + + + + + + + + + + SPFx On-Premises - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
+ +
+ +
+ +
+ +
+ + + + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ + +
+
+
+ + +
+
+ + + + + + + +

Workaround for on-premises SPFx TypeScript Version (SharePoint 2016 or 2019)

+

Note this article applies to version 1.4.1 SharePoint Framework projects targeting on-premises only. Also we have had reports that after version 2.0.9 of hte library this workaround no longer works.

+

When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premises it installs TypeScript version 2.2.2 (SP2016) or 2.4.2/2.4.1 (SP2019). Unfortunately this library relies on 3.6.4 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article.

+
npm i
+npm i -g rimraf # used to remove the node_modules folder (much better/faster)
+
+
    +
  1. Ensure that the @pnp/sp package is already installed npm i @pnp/sp
  2. +
  3. Remove the package-lock.json file & node_modules rimraf node_modules folder and execute npm install
  4. +
  5. Open package-lock.json from the root folder
  6. +
  7. Search for "typescript" or similar with version 2.4.1 (SP2019) 2.2.2 (SP2016)
  8. +
  9. Replace "2.4.1" or "2.2.2" with "3.6.4"
  10. +
  11. +

    Search for the next "typescript" occurrence and replace the block with:

    +

    JSON +"typescript": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", + "integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", + "dev": true +}

    +
  12. +
  13. +

    Remove node_modules folder rimraf node_modules

    +
  14. +
  15. Run npm install
  16. +
+

Alternative using npm-force-resolutions

+
    +
  1. +

    Install resolutions package and TypeScript providing considered version explicitly:

    +

    bash +npm i -D npm-force-resolutions typescript@3.6.4

    +
  2. +
  3. +

    Add a resolution for TypeScript and preinstall script into package.json to a corresponding code blocks:

    +

    JSON +{ + "scripts": { + "preinstall": "npx npm-force-resolutions" + }, + "resolutions": { + "typescript": "3.6.4" + } +}

    +
  4. +
  5. +

    Run npm install to trigger preinstall script and bumping TypeScript version into package-lock.json

    +
  6. +
  7. Run npm run build, should produce no errors
  8. +
+

Installing additional dependencies should be safe then.

+ + + + + + + +
+
+
+
+ + + + +
+ + + + + + + + + \ No newline at end of file diff --git a/v2/_theme/main.html b/v2/_theme/main.html new file mode 100644 index 000000000..2f621a908 --- /dev/null +++ b/v2/_theme/main.html @@ -0,0 +1,5 @@ +{% extends "base.html" %} + +{% block analytics %} + spacer +{% endblock %} diff --git a/v2/adaljsclient/adalclient/index.html b/v2/adaljsclient/adalclient/index.html new file mode 100644 index 000000000..7df21dfb6 --- /dev/null +++ b/v2/adaljsclient/adalclient/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/v2/adaljsclient/index.html b/v2/adaljsclient/index.html new file mode 100644 index 000000000..56ac110b8 --- /dev/null +++ b/v2/adaljsclient/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/v2/assets/images/favicon.png b/v2/assets/images/favicon.png new file mode 100644 index 000000000..1cf13b9f9 Binary files /dev/null and b/v2/assets/images/favicon.png differ diff --git a/v2/assets/javascripts/bundle.9554a270.min.js b/v2/assets/javascripts/bundle.9554a270.min.js new file mode 100644 index 000000000..a2089af3a --- /dev/null +++ b/v2/assets/javascripts/bundle.9554a270.min.js @@ -0,0 +1,2 @@ +!function(e,t){for(var n in t)e[n]=t[n]}(window,function(e){function t(t){for(var c,o,i=t[0],u=t[1],b=t[2],f=0,O=[];f"focus"===e),Object(d.a)(e===b()))}function m(e){return{x:e.scrollLeft,y:e.scrollTop}}function v(e){return Object(j.a)(Object(r.a)(e,"scroll"),Object(r.a)(window,"resize")).pipe(Object(p.a)(()=>m(e)),Object(d.a)(m(e)))}function g(e){if(!(e instanceof HTMLInputElement))throw new Error("Not implemented");e.select()}var $=n(61),y=n(32),w=n(62),x=n(46),S=n(82),C=n(20),T=n(64),k=n(75),E=n(65),A=n(83);const _=new y.a,L=Object(w.a)(()=>Object(x.a)(new $.a(e=>{for(const t of e)_.next(t)}))).pipe(Object(C.a)(e=>Object(j.a)(Object(x.a)(e),S.a).pipe(Object(T.a)(()=>e.disconnect()))),Object(k.a)({bufferSize:1,refCount:!0}));function R(e){return L.pipe(Object(E.a)(t=>t.observe(e)),Object(C.a)(t=>_.pipe(Object(A.a)(({target:t})=>t===e),Object(T.a)(()=>t.unobserve(e)),Object(p.a)(({contentRect:e})=>({width:e.width,height:e.height})))),Object(d.a)(function(e){return{width:e.offsetWidth,height:e.offsetHeight}}(e)))}var M=n(94);function z(e){switch(e.tagName){case"INPUT":case"SELECT":case"TEXTAREA":return!0;default:return e.isContentEditable}}function P(){return Object(r.a)(window,"keydown").pipe(Object(A.a)(e=>!(e.metaKey||e.ctrlKey)),Object(p.a)(e=>({type:e.key,claim(){e.preventDefault(),e.stopPropagation()}})),Object(M.a)())}var H=n(84);function U(e){location.href=e.href}function q(e,t=location){return e.host===t.host&&/^(?:\/[\w-]+)*(?:\/?|\.html)$/i.test(e.pathname)}function N(e,t=location){return e.pathname===t.pathname&&e.hash.length>0}function I(){return new H.a(new URL(location.href))}var D=n(85);function Y(e,{location$:t}){return t.pipe(Object(D.a)(1),Object(p.a)(({href:t})=>new URL(e,t).toString().replace(/\/$/,"")),Object(k.a)({bufferSize:1,refCount:!0}))}function B(){return location.hash.substring(1)}function F(e){const t=f("a");t.href=e,t.addEventListener("click",e=>e.stopPropagation()),t.click()}function J(){return Object(r.a)(window,"hashchange").pipe(Object(p.a)(B),Object(d.a)(B()),Object(A.a)(e=>e.length>0),Object(M.a)())}var K=n(6);function Q(e){const t=matchMedia(e);return new K.a(e=>{t.addListener(t=>e.next(t.matches))}).pipe(Object(d.a)(t.matches),Object(k.a)({bufferSize:1,refCount:!0}))}const W={drawer:u("[data-md-toggle=drawer]"),search:u("[data-md-toggle=search]")};function X(e){return W[e].checked}function V(e,t){W[e].checked!==t&&W[e].click()}function G(e){const t=W[e];return Object(r.a)(t,"change").pipe(Object(p.a)(()=>t.checked),Object(d.a)(t.checked))}var Z=n(47),ee=n(76);function te(){return{x:Math.max(0,pageXOffset),y:Math.max(0,pageYOffset)}}function ne({x:e,y:t}){window.scrollTo(e||0,t||0)}function ce(){return{width:innerWidth,height:innerHeight}}function re(){return Object(Z.a)([Object(j.a)(Object(r.a)(window,"scroll",{passive:!0}),Object(r.a)(window,"resize",{passive:!0})).pipe(Object(p.a)(te),Object(d.a)(te())),Object(r.a)(window,"resize",{passive:!0}).pipe(Object(p.a)(ce),Object(d.a)(ce()))]).pipe(Object(p.a)(([e,t])=>({offset:e,size:t})),Object(k.a)({bufferSize:1,refCount:!0}))}function ae(e,{header$:t,viewport$:n}){const c=n.pipe(Object(ee.a)("size")),r=Object(Z.a)([c,t]).pipe(Object(p.a)(()=>({x:e.offsetLeft,y:e.offsetTop})));return Object(Z.a)([t,n,r]).pipe(Object(p.a)(([{height:e},{offset:t,size:n},{x:c,y:r}])=>({offset:{x:t.x-c,y:t.y-r+e},size:n})))}var oe=n(86),ie=n(87);function ue(e,{tx$:t}){const n=Object(r.a)(e,"message").pipe(Object(p.a)(({data:e})=>e));return t.pipe(Object(oe.a)(()=>n,{leading:!0,trailing:!0}),Object(E.a)(t=>e.postMessage(t)),Object(ie.a)(n),Object(M.a)())}},,function(e,t,n){"use strict";function c(e){return"object"==typeof e&&"string"==typeof e.base&&"object"==typeof e.features&&"object"==typeof e.search}function r(e,t){if("string"==typeof t||"number"==typeof t)e.innerHTML+=t.toString();else if(t instanceof Node)e.appendChild(t);else if(Array.isArray(t))for(const n of t)r(e,n)}function a(e,t,...n){const c=document.createElement(e);if(t)for(const e of Object.keys(t))"boolean"!=typeof t[e]?c.setAttribute(e,t[e]):t[e]&&c.setAttribute(e,"");for(const e of n)r(c,e);return c}n.d(t,"d",(function(){return c})),n.d(t,"b",(function(){return a})),n.d(t,"a",(function(){return u})),n.d(t,"f",(function(){return f})),n.d(t,"g",(function(){return O})),n.d(t,"e",(function(){return j})),n.d(t,"c",(function(){return p}));var o=n(62),i=n(46);function u(e,t){return Object(o.a)(()=>{const n=sessionStorage.getItem(e);if(n)return Object(i.a)(JSON.parse(n));{const n=t();return n.subscribe(t=>{try{sessionStorage.setItem(e,JSON.stringify(t))}catch(e){}}),n}})}var b=n(0);let s;function f(e,t){if(void 0===s){const e=Object(b.d)("#__lang");s=JSON.parse(e.textContent)}if(void 0===s[e])throw new ReferenceError("Invalid translation: "+e);return void 0!==t?s[e].replace("#",t.toString()):s[e]}function O(e,t){let n=t;if(e.length>n){for(;" "!==e[n]&&--n>0;);return e.substring(0,n)+"..."}return e}function j(e){if(e>999){return((e+1e-6)/1e3).toFixed(+((e-950)%1e3>99))+"k"}return e.toString()}function p(e){let t=0;for(let n=0,c=e.length;n{Object(b.e)("pre > code").forEach((e,t)=>{const n=e.parentElement;n.id="__code_"+t,n.insertBefore(Object(s.a)(n.id),e)})});const n=new a.a(e=>{new c(".md-clipboard").on("success",t=>e.next(t))}).pipe(Object(o.a)());return n.pipe(Object(i.a)(e=>e.clearSelection()),Object(u.a)(Object(f.f)("clipboard.copied"))).subscribe(t),n}var j=n(32),p=n(46),d=n(68),l=n(18),h=n(20),m=n(11),v=n(67),g=n(97);function $({duration:e}={}){const t=new j.a,n=Object(b.a)("div");return n.classList.add("md-dialog","md-typeset"),t.pipe(Object(h.a)(t=>Object(p.a)(document.body).pipe(Object(m.a)(e=>e.appendChild(n)),Object(v.a)(d.a),Object(g.a)(1),Object(i.a)(e=>{e.innerHTML=t,e.setAttribute("data-md-state","open")}),Object(g.a)(e||2e3),Object(i.a)(e=>e.removeAttribute("data-md-state")),Object(g.a)(400),Object(i.a)(e=>{e.innerHTML="",e.remove()})))).subscribe(l.a),t}var y=n(80),w=n(96),x=n(9),S=n(83),C=n(36),T=n(76),k=n(89),E=n(90),A=n(88),_=n(77),L=n(91),R=n(78);function M(e,{document$:t,viewport$:n,location$:c}){"scrollRestoration"in history&&(history.scrollRestoration="manual"),Object(y.a)(window,"beforeunload").subscribe(()=>{history.scrollRestoration="auto"});const a=Object(b.c)('link[rel="shortcut icon"]');void 0!==a&&(a.href=a.href);const i=Object(y.a)(document.body,"click").pipe(Object(S.a)(e=>!(e.metaKey||e.ctrlKey)),Object(h.a)(t=>{if(t.target instanceof HTMLElement){const n=t.target.closest("a");if(n&&!n.target&&Object(b.h)(n)&&e.includes(n.href))return Object(b.g)(n)||t.preventDefault(),Object(p.a)(n)}return r.a}),Object(m.a)(e=>({url:new URL(e.href)})),Object(o.a)());i.subscribe(()=>{Object(b.o)("search",!1)});const u=i.pipe(Object(S.a)(({url:e})=>!Object(b.g)(e)),Object(o.a)()),s=Object(y.a)(window,"popstate").pipe(Object(S.a)(e=>null!==e.state),Object(m.a)(e=>({url:new URL(location.href),offset:e.state})),Object(o.a)());Object(w.a)(u,s).pipe(Object(C.a)((e,t)=>e.url.href===t.url.href),Object(m.a)(({url:e})=>e)).subscribe(c);const f=c.pipe(Object(T.a)("pathname"),Object(k.a)(1),Object(h.a)(e=>Object(x.a)(fetch(e.href,{credentials:"same-origin"}).then(e=>e.text())).pipe(Object(E.a)(()=>(Object(b.m)(e),r.a)))),Object(o.a)());u.pipe(Object(A.a)(f)).subscribe(({url:e})=>{history.pushState({},"",e.toString())});const O=new DOMParser;f.pipe(Object(m.a)(e=>O.parseFromString(e,"text/html"))).subscribe(t);const j=Object(w.a)(u,s).pipe(Object(A.a)(t));j.subscribe(({url:e,offset:t})=>{e.hash&&!t?Object(b.n)(e.hash):Object(b.p)(t||{y:0})}),j.pipe(Object(_.a)(t)).subscribe(([,{title:e,head:t}])=>{document.title=e;for(const e of['link[rel="canonical"]','meta[name="author"]','meta[name="description"]']){const n=Object(b.c)(e,t),c=Object(b.c)(e,document.head);void 0!==n&&void 0!==c&&Object(b.j)(c,n)}document.dispatchEvent(new CustomEvent("DOMContentSwitch"))}),n.pipe(Object(L.a)(250),Object(T.a)("offset")).subscribe(({offset:e})=>{history.replaceState(e,"")}),Object(w.a)(i,s).pipe(Object(R.a)(2,1),Object(S.a)(([e,t])=>e.url.pathname===t.url.pathname&&!Object(b.g)(t.url)),Object(m.a)(([,e])=>e)).subscribe(({offset:e})=>{Object(b.p)(e||{y:0})})}var z=n(8);function P(){const e=Object(b.u)().pipe(Object(m.a)(e=>Object.assign({mode:Object(b.f)("search")?"search":"global"},e)),Object(S.a)(({mode:e})=>{if("global"===e){const e=Object(b.b)();if(void 0!==e)return!Object(b.i)(e)}return!0}),Object(o.a)());return e.pipe(Object(S.a)(({mode:e})=>"search"===e),Object(_.a)(Object(z.useComponent)("search-query"),Object(z.useComponent)("search-result"))).subscribe(([e,t,n])=>{const c=Object(b.b)();switch(e.type){case"Enter":c===t&&e.claim();break;case"Escape":case"Tab":Object(b.o)("search",!1),Object(b.k)(t,!1);break;case"ArrowUp":case"ArrowDown":if(void 0===c)Object(b.k)(t);else{const r=[t,...Object(b.e)(":not(details) > [href], summary, details[open] [href]",n)],a=Math.max(0,(Math.max(0,r.indexOf(c))+r.length+("ArrowUp"===e.type?-1:1))%r.length);Object(b.k)(r[a])}e.claim();break;default:t!==Object(b.b)()&&Object(b.k)(t)}}),e.pipe(Object(S.a)(({mode:e})=>"global"===e),Object(_.a)(Object(z.useComponent)("search-query"))).subscribe(([e,t])=>{switch(e.type){case"f":case"s":case"/":Object(b.k)(t),Object(b.l)(t),e.claim();break;case"p":case",":const n=Object(b.c)("[href][rel=prev]");void 0!==n&&n.click();break;case"n":case".":const c=Object(b.c)("[href][rel=next]");void 0!==c&&c.click()}}),e}var H=n(35)},,,,,function(e,t,n){"use strict";n.d(t,"a",(function(){return O})),n.d(t,"b",(function(){return j}));var c=n(46),r=n(26),a=n(11),o=n(66),i=n(75),u=n(20),b=n(36),s=n(0);let f;function O(e,{document$:t}){f=t.pipe(Object(a.a)(t=>e.reduce((e,n)=>{const c=Object(s.c)(`[data-md-component=${n}]`,t);return Object.assign(Object.assign({},e),void 0!==c?{[n]:c}:{})},{})),Object(o.a)((t,n)=>{for(const c of e)switch(c){case"announce":case"header-title":case"container":case"skip":c in t&&void 0!==t[c]&&(Object(s.j)(t[c],n[c]),t[c]=n[c]);break;default:void 0!==n[c]?t[c]=Object(s.c)(`[data-md-component=${c}]`):delete t[c]}return t}),Object(i.a)({bufferSize:1,refCount:!0}))}function j(e){return f.pipe(Object(u.a)(t=>void 0!==t[e]?Object(c.a)(t[e]):r.a),Object(b.a)())}},,,,function(e,t,n){"use strict";function c(e,t){e.setAttribute("data-md-state",t?"blur":"")}function r(e){e.removeAttribute("data-md-state")}function a(e,t){e.classList.toggle("md-nav__link--active",t)}function o(e){e.classList.remove("md-nav__link--active")}n.d(t,"d",(function(){return c})),n.d(t,"b",(function(){return r})),n.d(t,"c",(function(){return a})),n.d(t,"a",(function(){return o}))},,,,function(e,t,n){"use strict";var c=n(49);n.o(c,"applySidebar")&&n.d(t,"applySidebar",(function(){return c.applySidebar})),n.o(c,"mountTableOfContents")&&n.d(t,"mountTableOfContents",(function(){return c.mountTableOfContents})),n.o(c,"mountTabs")&&n.d(t,"mountTabs",(function(){return c.mountTabs})),n.o(c,"watchSidebar")&&n.d(t,"watchSidebar",(function(){return c.watchSidebar}))},function(e,t,n){"use strict";n.d(t,"a",(function(){return a})),n.d(t,"b",(function(){return i})),n.d(t,"c",(function(){return u})),n.d(t,"d",(function(){return b}));var c,r=n(2);function a(e){return Object(r.b)("button",{class:"md-clipboard md-icon",title:Object(r.f)("clipboard.copy"),"data-clipboard-target":`#${e} > code`})}function o(e,t){const n=t&c.PARENT,a=t&c.TEASER,o=Object.keys(e.terms).filter(t=>!e.terms[t]).map(e=>[Object(r.b)("del",null,e)," "]).flat().slice(0,-1),i=e.location;return Object(r.b)("a",{href:i,class:"md-search-result__link",tabIndex:-1},Object(r.b)("article",{class:["md-search-result__article",...n?["md-search-result__article--document"]:[]].join(" "),"data-md-score":e.score.toFixed(2)},n>0&&Object(r.b)("div",{class:"md-search-result__icon md-icon"}),Object(r.b)("h1",{class:"md-search-result__title"},e.title),a>0&&e.text.length>0&&Object(r.b)("p",{class:"md-search-result__teaser"},Object(r.g)(e.text,320)),a>0&&o.length>0&&Object(r.b)("p",{class:"md-search-result__terms"},Object(r.f)("search.result.term.missing"),": ",o)))}function i(e,t=1/0){const n=[...e],a=n.findIndex(e=>!e.location.includes("#")),[i]=n.splice(a,1);let u=n.findIndex(e=>e.scoreo(e,c.TEASER)),...s.length?[Object(r.b)("details",{class:"md-search-result__more"},Object(r.b)("summary",{tabIndex:-1},s.length>0&&1===s.length?Object(r.f)("search.result.more.one"):Object(r.f)("search.result.more.other",s.length)),s.map(e=>o(e,c.TEASER)))]:[]];return Object(r.b)("li",{class:"md-search-result__item"},f)}function u(e){return Object(r.b)("ul",{class:"md-source__facts"},e.map(e=>Object(r.b)("li",{class:"md-source__fact"},e)))}function b(e){return Object(r.b)("div",{class:"md-typeset__scrollwrap"},Object(r.b)("div",{class:"md-typeset__table"},e))}!function(e){e[e.TEASER=1]="TEASER",e[e.PARENT=2]="PARENT"}(c||(c={}))},,,function(e,t,n){"use strict";function c(e,t){e.style.top=t+"px"}function r(e){e.style.top=""}function a(e,t){e.style.height=t+"px"}function o(e){e.style.height=""}n.d(t,"d",(function(){return c})),n.d(t,"b",(function(){return r})),n.d(t,"c",(function(){return a})),n.d(t,"a",(function(){return o}))},,,,,,,function(e,t,n){"use strict";var c=n(54);n.o(c,"applyAnchorList")&&n.d(t,"applyAnchorList",(function(){return c.applyAnchorList})),n.o(c,"watchAnchorList")&&n.d(t,"watchAnchorList",(function(){return c.watchAnchorList}));var r=n(55);n.d(t,"applyAnchorList",(function(){return r.a})),n.d(t,"watchAnchorList",(function(){return r.b}));n(19)},function(e,t,n){"use strict";n.d(t,"b",(function(){return c})),n.d(t,"f",(function(){return h})),n.d(t,"a",(function(){return r})),n.d(t,"d",(function(){return j})),n.d(t,"c",(function(){return p})),n.d(t,"e",(function(){return d}));n(63);function c(e){return e.split(/"([^"]+)"/g).map((e,t)=>1&t?e.replace(/^\b|^(?![^\x00-\x7F]|$)|\s+/g," +"):e).join("").replace(/"|(?:^|\s+)[*+\-:^~]+(?=\s+|$)/g,"").trim()}var r,a=n(32),o=n(42),i=n(77),u=n(11),b=n(94),s=n(67),f=n(0),O=n(2);function j(e){return e.type===r.READY}function p(e){return e.type===r.QUERY}function d(e){return e.type===r.RESULT}function l({config:e,docs:t,index:n}){1===e.lang.length&&"en"===e.lang[0]&&(e.lang=[Object(O.f)("search.config.lang")]),"[\\s\\-]+"===e.separator&&(e.separator=Object(O.f)("search.config.separator"));return{config:e,docs:t,index:n,pipeline:Object(O.f)("search.config.pipeline").split(/\s*,\s*/).filter(Boolean)}}function h(e,{index$:t,base$:n}){const c=new Worker(e),O=new a.a,j=Object(f.C)(c,{tx$:O}).pipe(Object(i.a)(n),Object(u.a)(([e,t])=>{if(d(e))for(const n of e.data)for(const e of n)e.location=`${t}/${e.location}`;return e}),Object(b.a)());return t.pipe(Object(u.a)(e=>({type:r.SETUP,data:l(e)})),Object(s.a)(o.a)).subscribe(O.next.bind(O)),{tx$:O,rx$:j}}!function(e){e[e.SETUP=0]="SETUP",e[e.READY=1]="READY",e[e.QUERY=2]="QUERY",e[e.RESULT=3]="RESULT"}(r||(r={}))},,,,,,,,,,,,,function(e,t,n){"use strict";n.d(t,"a",(function(){return u}));var c=n(33),r=n(46),a=n(20),o=n(11),i=n(23);function u({header$:e,main$:t,viewport$:n,screen$:u}){return Object(c.a)(Object(a.a)(c=>u.pipe(Object(a.a)(a=>a?Object(i.watchSidebar)(c,{main$:t,viewport$:n}).pipe(Object(i.applySidebar)(c,{header$:e}),Object(o.a)(e=>({sidebar:e}))):Object(r.a)({})))))}},function(e,t,n){"use strict";var c=n(50);n.o(c,"applySidebar")&&n.d(t,"applySidebar",(function(){return c.applySidebar})),n.o(c,"mountTableOfContents")&&n.d(t,"mountTableOfContents",(function(){return c.mountTableOfContents})),n.o(c,"mountTabs")&&n.d(t,"mountTabs",(function(){return c.mountTabs})),n.o(c,"watchSidebar")&&n.d(t,"watchSidebar",(function(){return c.watchSidebar}));var r=n(51);n.d(t,"applySidebar",(function(){return r.a})),n.d(t,"watchSidebar",(function(){return r.b}));n(27)},function(e,t){},function(e,t,n){"use strict";n.d(t,"b",(function(){return j})),n.d(t,"a",(function(){return p}));var c=n(47),r=n(33),a=n(68),o=n(11),i=n(36),u=n(67),b=n(77),s=n(65),f=n(64),O=n(27);function j(e,{main$:t,viewport$:n}){const r=e.parentElement.offsetTop-e.parentElement.parentElement.offsetTop;return Object(c.a)([t,n]).pipe(Object(o.a)(([{offset:e,height:t},{offset:{y:n}}])=>({height:t=t+Math.min(r,Math.max(0,n-e))-r,lock:n>=e+r})),Object(i.a)((e,t)=>e.height===t.height&&e.lock===t.lock))}function p(e,{header$:t}){return Object(r.a)(Object(u.a)(a.a),Object(b.a)(t),Object(s.a)(([{height:t,lock:n},{height:c}])=>{Object(O.c)(e,t),n?Object(O.d)(e,c):Object(O.b)(e)}),Object(o.a)(([e])=>e),Object(f.a)(()=>{Object(O.b)(e),Object(O.a)(e)}))}},function(e,t,n){"use strict";var c=n(53);n.d(t,"mountTableOfContents",(function(){return c.a}));n(34)},function(e,t,n){"use strict";n.d(t,"a",(function(){return f}));var c=n(33),r=n(47),a=n(46),o=n(20),i=n(11),u=n(0),b=n(23),s=n(34);function f({header$:e,main$:t,viewport$:n,tablet$:f}){return Object(c.a)(Object(o.a)(c=>f.pipe(Object(o.a)(o=>{if(o){const a=Object(u.e)(".md-nav__link",c),o=Object(b.watchSidebar)(c,{main$:t,viewport$:n}).pipe(Object(b.applySidebar)(c,{header$:e})),f=Object(s.watchAnchorList)(a,{header$:e,viewport$:n}).pipe(Object(s.applyAnchorList)(a));return Object(r.a)([o,f]).pipe(Object(i.a)(([e,t])=>({sidebar:e,anchors:t})))}return Object(a.a)({})}))))}},function(e,t){},function(e,t,n){"use strict";n.d(t,"b",(function(){return m})),n.d(t,"a",(function(){return v}));var c=n(47),r=n(33),a=n(68),o=n(11),i=n(76),u=n(20),b=n(66),s=n(36),f=n(79),O=n(78),j=n(67),p=n(65),d=n(64),l=n(0),h=n(19);function m(e,{header$:t,viewport$:n}){const r=new Map;for(const t of e){const e=decodeURIComponent(t.hash.substring(1)),n=Object(l.c)(`[id="${e}"]`);void 0!==n&&r.set(t,n)}const a=t.pipe(Object(o.a)(e=>18+e.height));return Object(l.t)(document.body).pipe(Object(i.a)("height"),Object(o.a)(()=>{let e=[];return[...r].reduce((t,[n,c])=>{for(;e.length;){if(!(r.get(e[e.length-1]).tagName>=c.tagName))break;e.pop()}let a=c.offsetTop;for(;!a&&c.parentElement;)a=(c=c.parentElement).offsetTop;return t.set([...e=[...e,n]].reverse(),a)},new Map)}),Object(u.a)(e=>Object(c.a)([a,n]).pipe(Object(b.a)(([e,t],[n,{offset:{y:c}}])=>{for(;t.length;){const[,r]=t[0];if(!(r-n=c))break;t=[e.pop(),...t]}return[e,t]},[[],[...e]]),Object(s.a)((e,t)=>e[0]===t[0]&&e[1]===t[1])))).pipe(Object(o.a)(([e,t])=>({prev:e.map(([e])=>e),next:t.map(([e])=>e)})),Object(f.a)({prev:[],next:[]}),Object(O.a)(2,1),Object(o.a)(([e,t])=>e.prev.length{for(const[e]of t)Object(h.a)(e),Object(h.b)(e);e.forEach(([t],n)=>{Object(h.c)(t,n===e.length-1),Object(h.d)(t,!0)})}),Object(d.a)(()=>{for(const t of e)Object(h.a)(t),Object(h.b)(t)}))}},function(e,t,n){"use strict";n.d(t,"a",(function(){return j})),n.d(t,"b",(function(){return $})),n.d(t,"c",(function(){return S})),n.d(t,"d",(function(){return z}));var c=n(33),r=n(47),a=n(20),o=n(83),i=n(81),u=n(79),b=n(88),s=n(85),f=n(11),O=n(35);function j({rx$:e,tx$:t},{query$:n,reset$:j,result$:p}){return Object(c.a)(Object(a.a)(()=>{const c=e.pipe(Object(o.a)(O.d),Object(i.a)("ready"),Object(u.a)("waiting"));return t.pipe(Object(o.a)(O.c),Object(b.a)(c),Object(s.a)(1)).subscribe(t.next.bind(t)),Object(r.a)([c,n,p,j]).pipe(Object(f.a)(([e,t,n])=>({status:e,query:t,result:n})))}))}var p=n(76),d=n(0),l=n(10),h=n(96),m=n(80),v=n(97),g=n(36);function $({tx$:e},t={}){return Object(c.a)(Object(a.a)(n=>{const c=function(e,{transform:t}={}){const n=t||l.b,c=Object(h.a)(Object(m.a)(e,"keyup"),Object(m.a)(e,"focus").pipe(Object(v.a)(1))).pipe(Object(f.a)(()=>n(e.value)),Object(u.a)(n(e.value)),Object(g.a)()),a=Object(d.r)(e);return Object(r.a)([c,a]).pipe(Object(f.a)(([e,t])=>({value:e,focus:t})))}(n,t);return c.pipe(Object(p.a)("value"),Object(f.a)(({value:e})=>({type:l.a.QUERY,data:e}))).subscribe(e.next.bind(e)),c.pipe(Object(p.a)("focus")).subscribe(({focus:e})=>{e&&Object(d.o)("search",e)}),c}))}var y=n(87),w=n(65),x=n(15);function S(){return Object(c.a)(Object(a.a)(e=>function(e){return Object(m.a)(e,"click").pipe(Object(i.a)(void 0))}(e).pipe(Object(y.a)(Object(x.b)("search-query")),Object(w.a)(d.k),Object(i.a)(void 0))),Object(u.a)(void 0))}var C=n(68),T=n(77),k=n(67),E=n(66),A=n(64),_=n(24),L=n(2);function R(e,t){e.appendChild(t)}function M(e,{query$:t,ready$:n,fetch$:r}){const o=Object(d.d)(".md-search-result__list",e),u=Object(d.d)(".md-search-result__meta",e);return Object(c.a)(Object(T.a)(t,n),Object(f.a)(([e,t])=>(t.value?function(e,t){switch(t){case 0:e.textContent=Object(L.f)("search.result.none");break;case 1:e.textContent=Object(L.f)("search.result.one");break;default:e.textContent=Object(L.f)("search.result.other",t)}}(u,e.length):function(e){e.textContent=Object(L.f)("search.result.placeholder")}(u),e)),Object(a.a)(t=>{const n=[...t.map(([e])=>e.score),0];return r.pipe(Object(k.a)(C.a),Object(E.a)(c=>{const r=e.parentElement;for(;c16)););return c},0),Object(i.a)(t),Object(A.a)(()=>{!function(e){e.innerHTML=""}(o)}))}))}function z({rx$:e},{query$:t}){return Object(c.a)(Object(a.a)(n=>{const c=n.parentElement,r=e.pipe(Object(o.a)(l.c),Object(i.a)(!0)),a=Object(d.s)(c).pipe(Object(f.a)(({y:e})=>e>=c.scrollHeight-c.offsetHeight-16),Object(g.a)(),Object(o.a)(Boolean));return e.pipe(Object(o.a)(l.d),Object(f.a)(({data:e})=>e),M(n,{query$:t,ready$:r,fetch$:a}),Object(u.a)([]))}))}},function(e,t,n){"use strict";n.d(t,"a",(function(){return v}));var c=n(33),r=n(47),a=n(20),o=n(11),i=n(83),u=n(95),b=n(36),s=n(79),f=n(0),O=n(15),j=n(46),p=n(68),d=n(75),l=n(67),h=n(65),m=n(64);function v({document$:e,viewport$:t}){return Object(c.a)(Object(a.a)(n=>{const v=function(e,{document$:t}){return t.pipe(Object(o.a)(()=>{const t=getComputedStyle(e);return["sticky","-webkit-sticky"].includes(t.position)}),Object(b.a)(),Object(a.a)(t=>t?Object(f.t)(e).pipe(Object(o.a)(({height:e})=>({sticky:!0,height:e}))):Object(j.a)({sticky:!1,height:0})),Object(d.a)({bufferSize:1,refCount:!0}))}(n,{document$:e}),g=Object(O.b)("main").pipe(Object(o.a)(e=>Object(f.c)("h1, h2, h3, h4, h5, h6",e)),Object(i.a)(e=>void 0!==e),Object(u.a)(Object(O.b)("header-title")),Object(a.a)(([e,n])=>Object(f.B)(e,{header$:v,viewport$:t}).pipe(Object(o.a)(({offset:{y:t}})=>t>=e.offsetHeight?"page":"site"),Object(b.a)(),function(e){return Object(c.a)(Object(l.a)(p.a),Object(h.a)(t=>{!function(e,t){e.setAttribute("data-md-state",t?"active":"")}(e,"page"===t)}),Object(m.a)(()=>{!function(e){e.removeAttribute("data-md-state")}(e)}))}(n))),Object(s.a)("site"));return Object(r.a)([v,g]).pipe(Object(o.a)(([e,t])=>Object.assign({type:t},e)))}))}},function(e,t,n){"use strict";n.d(t,"a",(function(){return h}));var c=n(32),r=n(18),a=n(33),o=n(20),i=n(76),u=n(65),b=n(64),s=n(15),f=n(47),O=n(68),j=n(11),p=n(36),d=n(67),l=n(0);function h({header$:e,viewport$:t}){const n=new c.a;return Object(s.b)("header").pipe(Object(o.a)(e=>{return n.pipe(Object(i.a)("active"),(t=e,Object(a.a)(Object(d.a)(O.a),Object(u.a)(({active:e})=>{!function(e,t){e.setAttribute("data-md-state",t?"shadow":"")}(t,e)}),Object(b.a)(()=>{!function(e){e.removeAttribute("data-md-state")}(t)}))));var t})).subscribe(r.a),Object(a.a)(Object(o.a)(n=>function(e,{header$:t,viewport$:n}){const c=t.pipe(Object(j.a)(({height:e})=>e),Object(p.a)()),r=c.pipe(Object(o.a)(()=>Object(l.t)(e).pipe(Object(j.a)(({height:t})=>({top:e.offsetTop,bottom:e.offsetTop+t})),Object(i.a)("bottom"))));return Object(f.a)([c,r,n]).pipe(Object(j.a)(([e,{top:t,bottom:n},{offset:{y:c},size:{height:r}}])=>({offset:t-e,height:r=Math.max(0,r-Math.max(0,t-c,e)-Math.max(0,r+c-n)),active:t-e<=c})),Object(p.a)((e,t)=>e.offset===t.offset&&e.height===t.height&&e.active===t.active))}(n,{header$:e,viewport$:t})),Object(u.a)(e=>n.next(e)),Object(b.a)(()=>n.complete()))}},function(e,t,n){"use strict";n.d(t,"a",(function(){return j}));var c=n(33),r=n(46),a=n(20),o=n(11),i=n(76),u=n(0),b=n(68),s=n(67),f=n(65),O=n(64);function j({header$:e,viewport$:t,screen$:n}){return Object(c.a)(Object(a.a)(j=>n.pipe(Object(a.a)(n=>n?Object(u.B)(j,{header$:e,viewport$:t}).pipe(Object(o.a)(({offset:{y:e}})=>({hidden:e>=10})),Object(i.a)("hidden"),function(e){return Object(c.a)(Object(s.a)(b.a),Object(f.a)(({hidden:t})=>{!function(e,t){e.setAttribute("data-md-state",t?"hidden":"")}(e,t)}),Object(O.a)(()=>{!function(e){e.removeAttribute("data-md-state")}(e)}))}(j)):Object(r.a)({hidden:!0})))))}},,,,,,,,,,,,,,,function(e,t,n){"use strict";n.r(t),n.d(t,"setScrollLock",(function(){return H})),n.d(t,"resetScrollLock",(function(){return U})),n.d(t,"initialize",(function(){return q}));n(69);var c=n(62),r=n(9),a=n(46),o=n(82),i=n(47),u=n(68),b=n(80),s=n(96),f=n(75),O=n(20),j=n(90),p=n(65),d=n(97),l=n(77),h=n(67),m=n(83),v=n(11),g=n(85),$=n(0),y=n(8),w=n(10),x=n(76);var S=n(87);var C=n(6),T=n(26),k=n(18),E=n(89),A=n(92);var _=n(93),L=n(81);function R(){return/(iPad|iPhone|iPod)/.test(navigator.userAgent)}var M=n(24),z=n(2);function P(e){const[t]=e.match(/(git(?:hub|lab))/i)||[];switch(t.toLowerCase()){case"github":const[,t,n]=e.match(/^.+github\.com\/([^\/]+)\/?([^\/]+)?/i);return function(e,t){const n=void 0!==t?`https://api.github.com/repos/${e}/${t}`:"https://api.github.com/users/"+e;return Object(r.a)(fetch(n).then(e=>e.json())).pipe(Object(v.a)(e=>{if(void 0!==t){const{stargazers_count:t,forks_count:n}=e;return[Object(z.e)(t||0)+" Stars",Object(z.e)(n||0)+" Forks"]}{const{public_repos:t}=e;return[Object(z.e)(t||0)+" Repositories"]}}))}(t,n);case"gitlab":const[,c,a]=e.match(/^.+?([^\/]*gitlab[^\/]+)\/(.+?)\/?$/i);return function(e,t){const n=`https://${e}/api/v4/projects/${encodeURIComponent(t)}`;return Object(r.a)(fetch(n).then(e=>e.json())).pipe(Object(v.a)(({star_count:e,forks_count:t})=>[Object(z.e)(e)+" Stars",Object(z.e)(t)+" Forks"]))}(c,a);default:return o.a}}function H(e,t){e.setAttribute("data-md-state","lock"),e.style.top=`-${t}px`}function U(e){const t=-1*parseInt(e.style.top,10);e.removeAttribute("data-md-state"),e.style.top="",t&&window.scrollTo(0,t)}function q(e){if(!Object(z.d)(e))throw new SyntaxError("Invalid configuration: "+JSON.stringify(e));const t=Object($.q)(),n=Object($.v)(),q=Object($.w)(e.base,{location$:n}),N=Object($.x)(),I=Object($.A)(),D=Object($.y)("(min-width: 960px)"),Y=Object($.y)("(min-width: 1220px)");Object(y.setupComponents)(["announce","container","header","header-title","main","navigation","search","search-query","search-reset","search-result","skip","tabs","toc"],{document$:t});const B=Object(w.h)();matchMedia("(hover)").matches&&function({document$:e,viewport$:t}){const n=e.pipe(Object(v.a)(()=>Object($.e)("pre > code"))),c=t.pipe(Object(x.a)("size"));Object(i.a)([n,c]).subscribe(([e])=>{for(const t of e)t.scrollWidth>t.clientWidth?t.setAttribute("tabindex","0"):t.removeAttribute("tabindex")})}({document$:t,viewport$:I}),function({document$:e,hash$:t}){const n=e.pipe(Object(v.a)(()=>Object($.e)("details")));Object(s.a)(Object($.y)("print").pipe(Object(m.a)(Boolean)),Object(b.a)(window,"beforeprint")).pipe(Object(S.a)(n)).subscribe(e=>{for(const t of e)t.setAttribute("open","")}),t.pipe(Object(v.a)(e=>Object($.c)(`[id="${e}"]`)),Object(m.a)(e=>void 0!==e),Object(p.a)(e=>{const t=e.closest("details");t&&!t.open&&t.setAttribute("open","")})).subscribe(e=>e.scrollIntoView())}({document$:t,hash$:N}),function({document$:e}){e.pipe(Object(E.a)(1),Object(l.a)(Object(y.useComponent)("container")),Object(v.a)(([,e])=>Object($.e)("script",e))).pipe(Object(O.a)(e=>Object(a.a)(...e)),Object(A.a)(e=>{const t=Object($.a)("script");return e.src?(t.src=e.src,Object($.j)(e,t),new C.a(e=>{t.onload=()=>e.complete()})):(t.textContent=e.textContent,Object($.j)(e,t),T.a)})).subscribe(k.a)}({document$:t}),function({document$:e}){e.pipe(Object(v.a)(()=>Object($.d)(".md-source[href]")),Object(O.a)(({href:e})=>Object(z.a)(""+Object(z.c)(e),()=>P(e))),Object(j.a)(()=>o.a)).subscribe(e=>{for(const t of Object($.e)(".md-source__repository"))t.hasAttribute("data-md-state")||(t.setAttribute("data-md-state","done"),t.appendChild(Object(M.c)(e)))})}({document$:t}),function({document$:e}){const t=Object($.a)("table");e.pipe(Object(v.a)(()=>Object($.e)("table:not([class])"))).subscribe(e=>{for(const n of e)Object($.j)(n,t),Object($.j)(t,Object(M.d)(n))})}({document$:t}),function({document$:e}){const t=e.pipe(Object(v.a)(()=>Object($.e)("[data-md-scrollfix]")),Object(f.a)({bufferSize:1,refCount:!0}));t.subscribe(e=>{for(const t of e)t.removeAttribute("data-md-scrollfix")}),Object(_.a)(R,t,o.a).pipe(Object(O.a)(e=>Object(s.a)(...e.map(e=>Object(b.a)(e,"touchstart").pipe(Object(L.a)(e)))))).subscribe(e=>{const t=e.scrollTop;0===t?e.scrollTop=1:t+e.offsetHeight===e.scrollHeight&&(e.scrollTop=t-1)})}({document$:t});const F=Object(w.f)(),J=Object(w.e)({document$:t,dialog$:F}),K=Object(y.useComponent)("header").pipe(Object(y.mountHeader)({document$:t,viewport$:I}),Object(f.a)({bufferSize:1,refCount:!0})),Q=Object(y.useComponent)("main").pipe(Object(y.mountMain)({header$:K,viewport$:I}),Object(f.a)({bufferSize:1,refCount:!0})),W=Object(y.useComponent)("navigation").pipe(Object(y.mountNavigation)({header$:K,main$:Q,viewport$:I,screen$:Y}),Object(f.a)({bufferSize:1,refCount:!0})),X=Object(y.useComponent)("toc").pipe(Object(y.mountTableOfContents)({header$:K,main$:Q,viewport$:I,tablet$:D}),Object(f.a)({bufferSize:1,refCount:!0})),V=Object(y.useComponent)("tabs").pipe(Object(y.mountTabs)({header$:K,viewport$:I,screen$:Y}),Object(f.a)({bufferSize:1,refCount:!0})),G=Object(y.useComponent)("search").pipe(Object(O.a)(()=>Object(c.a)(()=>{const t=e.search&&e.search.index?e.search.index:void 0,n=void 0!==t?Object(r.a)(t):q.pipe(Object(O.a)(e=>fetch(e+"/search/search_index.json",{credentials:"same-origin"}).then(e=>e.json())));return Object(a.a)(Object(w.i)(e.search.worker,{base$:q,index$:n}))}))).pipe(Object(O.a)(t=>{const n=Object(y.useComponent)("search-query").pipe(Object(y.mountSearchQuery)(t,{transform:e.search.transform}),Object(f.a)({bufferSize:1,refCount:!0})),c=Object(y.useComponent)("search-reset").pipe(Object(y.mountSearchReset)(),Object(f.a)({bufferSize:1,refCount:!0})),r=Object(y.useComponent)("search-result").pipe(Object(y.mountSearchResult)(t,{query$:n}),Object(f.a)({bufferSize:1,refCount:!0}));return Object(y.useComponent)("search").pipe(Object(y.mountSearch)(t,{query$:n,reset$:c,result$:r}))}),Object(j.a)(()=>(Object(y.useComponent)("search").subscribe(e=>e.hidden=!0),o.a)),Object(f.a)({bufferSize:1,refCount:!0}));if(N.pipe(Object(p.a)(()=>Object($.o)("search",!1)),Object(d.a)(125)).subscribe(e=>Object($.n)("#"+e)),Object(i.a)([Object($.z)("search"),D]).pipe(Object(l.a)(I),Object(O.a)(([[e,n],{offset:{y:c}}])=>{const r=e&&!n;return t.pipe(Object(d.a)(r?400:100),Object(h.a)(u.a),Object(p.a)(({body:e})=>r?H(e,c):U(e)))})).subscribe(),Object(b.a)(document.body,"click").pipe(Object(m.a)(e=>!(e.metaKey||e.ctrlKey)),Object(m.a)(e=>{if(e.target instanceof HTMLElement){const t=e.target.closest("a");if(t&&Object($.h)(t))return!0}return!1})).subscribe(()=>{Object($.o)("drawer",!1)}),e.features.includes("navigation.instant")&&"file:"!==location.protocol){const e=new DOMParser;q.pipe(Object(O.a)(t=>Object(r.a)(fetch(t+"/sitemap.xml").then(e=>e.text()).then(t=>e.parseFromString(t,"text/xml")))),Object(l.a)(q),Object(v.a)(([e,t])=>{const n=Object($.e)("loc",e).map(e=>e.textContent);if(n.length>1){const[e,c]=n.sort((e,t)=>e.length-t.length);let r=0;if(e===c)r=e.length;else for(;e.charAt(r)===c.charAt(r);)r++;for(let c=0;c{Object(w.g)(e,{document$:t,location$:n,viewport$:I})})}B.pipe(Object(m.a)(e=>"global"===e.mode&&"Tab"===e.type),Object(g.a)(1)).subscribe(()=>{for(const e of Object($.e)(".headerlink"))e.style.visibility="visible"});const Z={document$:t,location$:n,viewport$:I,header$:K,main$:Q,navigation$:W,search$:G,tabs$:V,toc$:X,clipboard$:J,keyboard$:B,dialog$:F};return Object(s.a)(...Object.values(Z)).subscribe(),Z}document.documentElement.classList.remove("no-js"),document.documentElement.classList.add("js"),navigator.userAgent.match(/(iPad|iPhone|iPod)/g)&&document.documentElement.classList.add("ios")}])); +//# sourceMappingURL=bundle.9554a270.min.js.map \ No newline at end of file diff --git a/v2/assets/javascripts/bundle.9554a270.min.js.map b/v2/assets/javascripts/bundle.9554a270.min.js.map new file mode 100644 index 000000000..d28a93f79 --- /dev/null +++ b/v2/assets/javascripts/bundle.9554a270.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./src/assets/javascripts/browser/document/index.ts","webpack:///./src/assets/javascripts/browser/element/_/index.ts","webpack:///./src/assets/javascripts/browser/element/focus/index.ts","webpack:///./src/assets/javascripts/browser/element/offset/index.ts","webpack:///./src/assets/javascripts/browser/element/select/index.ts","webpack:///./src/assets/javascripts/browser/element/size/index.ts","webpack:///./src/assets/javascripts/browser/keyboard/index.ts","webpack:///./src/assets/javascripts/browser/location/_/index.ts","webpack:///./src/assets/javascripts/browser/location/base/index.ts","webpack:///./src/assets/javascripts/browser/location/hash/index.ts","webpack:///./src/assets/javascripts/browser/media/index.ts","webpack:///./src/assets/javascripts/browser/toggle/index.ts","webpack:///./src/assets/javascripts/browser/viewport/offset/index.ts","webpack:///./src/assets/javascripts/browser/viewport/size/index.ts","webpack:///./src/assets/javascripts/browser/viewport/_/index.ts","webpack:///./src/assets/javascripts/browser/worker/index.ts","webpack:///./src/assets/javascripts/utilities/config/index.ts","webpack:///./src/assets/javascripts/utilities/jsx/index.ts","webpack:///./src/assets/javascripts/utilities/rxjs/index.ts","webpack:///./src/assets/javascripts/utilities/string/index.ts","webpack:///./src/assets/javascripts/components/index.ts","webpack:///./src/assets/javascripts/integrations/clipboard/index.ts","webpack:///./src/assets/javascripts/integrations/dialog/index.ts","webpack:///./src/assets/javascripts/integrations/instant/index.ts","webpack:///./src/assets/javascripts/integrations/keyboard/index.ts","webpack:///./src/assets/javascripts/components/_/index.ts","webpack:///./src/assets/javascripts/components/toc/anchor/set/index.ts","webpack:///./src/assets/javascripts/components/shared/index.ts","webpack:///./src/assets/javascripts/templates/search/index.tsx","webpack:///./src/assets/javascripts/templates/clipboard/index.tsx","webpack:///./src/assets/javascripts/templates/source/index.tsx","webpack:///./src/assets/javascripts/templates/table/index.tsx","webpack:///./src/assets/javascripts/components/shared/sidebar/set/index.ts","webpack:///./src/assets/javascripts/components/toc/anchor/index.ts","webpack:///./src/assets/javascripts/integrations/search/query/transform/index.ts","webpack:///./src/assets/javascripts/integrations/search/worker/message/index.ts","webpack:///./src/assets/javascripts/integrations/search/worker/_/index.ts","webpack:///./src/assets/javascripts/components/navigation/index.ts","webpack:///./src/assets/javascripts/components/shared/sidebar/index.ts","webpack:///./src/assets/javascripts/components/shared/sidebar/react/index.ts","webpack:///./src/assets/javascripts/components/toc/index.ts","webpack:///./src/assets/javascripts/components/toc/_/index.ts","webpack:///./src/assets/javascripts/components/toc/anchor/react/index.ts","webpack:///./src/assets/javascripts/components/search/_/index.ts","webpack:///./src/assets/javascripts/components/search/query/_/index.ts","webpack:///./src/assets/javascripts/components/search/query/react/index.ts","webpack:///./src/assets/javascripts/components/search/reset/_/index.ts","webpack:///./src/assets/javascripts/components/search/reset/react/index.ts","webpack:///./src/assets/javascripts/components/search/result/set/index.ts","webpack:///./src/assets/javascripts/components/search/result/react/index.ts","webpack:///./src/assets/javascripts/components/search/result/_/index.ts","webpack:///./src/assets/javascripts/components/header/_/index.ts","webpack:///./src/assets/javascripts/components/header/react/index.ts","webpack:///./src/assets/javascripts/components/header/set/index.ts","webpack:///./src/assets/javascripts/components/main/_/index.ts","webpack:///./src/assets/javascripts/components/main/react/index.ts","webpack:///./src/assets/javascripts/components/main/set/index.ts","webpack:///./src/assets/javascripts/components/tabs/_/index.ts","webpack:///./src/assets/javascripts/components/tabs/react/index.ts","webpack:///./src/assets/javascripts/components/tabs/set/index.ts","webpack:///./src/assets/javascripts/patches/scrollfix/index.ts","webpack:///./src/assets/javascripts/patches/source/index.ts","webpack:///./src/assets/javascripts/patches/source/github/index.ts","webpack:///./src/assets/javascripts/patches/source/gitlab/index.ts","webpack:///./src/assets/javascripts/index.ts","webpack:///./src/assets/javascripts/patches/code/index.ts","webpack:///./src/assets/javascripts/patches/details/index.ts","webpack:///./src/assets/javascripts/patches/script/index.ts","webpack:///./src/assets/javascripts/patches/table/index.ts"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","0","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","window","oldJsonpFunction","slice","watchDocument","document$","ReplaySubject","fromEvent","document","pipe","mapTo","subscribe","getElement","selector","node","querySelector","undefined","getElementOrThrow","el","ReferenceError","getActiveElement","activeElement","HTMLElement","getElements","Array","from","querySelectorAll","createElement","tagName","replaceElement","source","target","replaceWith","setElementFocus","focus","blur","watchElementFocus","merge","map","type","startWith","getElementOffset","x","scrollLeft","y","scrollTop","watchElementOffset","setElementSelection","HTMLInputElement","Error","select","entry$","Subject","observer$","defer","of","entries","entry","next","switchMap","resize","finalize","disconnect","shareReplay","bufferSize","refCount","watchElementSize","tap","observer","observe","filter","unobserve","contentRect","width","height","offsetWidth","offsetHeight","getElementSize","isSusceptibleToKeyboard","isContentEditable","watchKeyboard","ev","metaKey","ctrlKey","preventDefault","stopPropagation","share","setLocation","url","location","href","isLocalLocation","ref","host","test","pathname","isAnchorLocation","hash","watchLocation","BehaviorSubject","URL","watchLocationBase","base","location$","take","toString","replace","getLocationHash","substring","setLocationHash","addEventListener","click","watchLocationHash","watchMedia","query","media","matchMedia","Observable","subscriber","addListener","matches","toggles","drawer","search","getToggle","checked","setToggle","watchToggle","getViewportOffset","Math","max","pageXOffset","pageYOffset","setViewportOffset","scrollTo","getViewportSize","innerWidth","innerHeight","watchViewport","combineLatest","passive","offset","size","watchViewportAt","header$","viewport$","size$","distinctUntilKeyChanged","offset$","offsetLeft","offsetTop","watchWorker","worker","tx$","rx$","throttle","leading","trailing","message","postMessage","switchMapTo","isConfig","config","features","appendChild","child","innerHTML","Node","isArray","h","tag","attributes","children","attr","keys","setAttribute","cache","factory","sessionStorage","getItem","JSON","parse","value$","setItem","stringify","err","lang","translate","textContent","truncate","round","toFixed","len","charCodeAt","setupClipboard","dialog$","forEach","block","index","parent","parentElement","id","insertBefore","clipboard$","on","clearSelection","setupDialog","duration","dialog","classList","add","text","body","container","observeOn","animationFrame","delay","removeAttribute","remove","noop","setupInstantLoading","urls","history","scrollRestoration","favicon","state$","closest","includes","push$","pop$","state","distinctUntilChanged","prev","ajax$","skip","fetch","credentials","then","res","catchError","sample","pushState","dom","DOMParser","response","parseFromString","instant$","withLatestFrom","title","head","dispatchEvent","CustomEvent","debounceTime","replaceState","bufferCount","setupKeyboard","keyboard$","active","claim","els","indexOf","components$","setupComponents","names","reduce","components","useComponent","setAnchorBlur","resetAnchorBlur","setAnchorActive","toggle","resetAnchorActive","Flag","renderClipboardButton","class","renderSearchDocument","flag","PARENT","teaser","TEASER","missing","terms","flat","tabIndex","join","score","renderSearchResult","threshold","Infinity","docs","findIndex","doc","article","best","more","section","renderSource","facts","fact","renderTable","table","setSidebarOffset","style","top","resetSidebarOffset","setSidebarHeight","resetSidebarHeight","defaultTransform","split","trim","SearchMessageType","isSearchReadyMessage","READY","isSearchQueryMessage","QUERY","isSearchResultMessage","RESULT","setupSearchIndex","separator","pipeline","Boolean","setupSearchWorker","index$","base$","Worker","SETUP","mountNavigation","main$","screen$","screen","sidebar","watchSidebar","adjust","min","lock","a","b","applySidebar","mountTableOfContents","tablet$","tablet","sidebar$","anchors$","anchors","watchAnchorList","Map","decodeURIComponent","set","adjust$","header","path","anchor","pop","reverse","applyAnchorList","mountSearch","query$","reset$","result$","status$","status","mountSearchQuery","options","transform","fn","focus$","watchSearchQuery","mountSearchReset","watchSearchReset","addToSearchResultList","applySearchResult","ready$","fetch$","list","meta","setSearchResultMeta","resetSearchResultMeta","thresholds","scan","scrollHeight","resetSearchResultList","mountSearchResult","mountHeader","styles","getComputedStyle","position","sticky","watchHeader","type$","main","hx","zipWith","setHeaderTitleActive","resetHeaderTitleActive","applyHeaderType","mountMain","setHeaderShadow","resetHeaderShadow","border$","bottom","watchMain","complete","mountTabs","hidden","setTabsHidden","resetTabsHidden","applyTabs","isAppleDevice","navigator","userAgent","fetchSourceFacts","match","toLowerCase","user","repo","json","stargazers_count","forks_count","public_repos","fetchSourceFactsFromGitHub","slug","project","encodeURIComponent","star_count","fetchSourceFactsFromGitLab","setScrollLock","resetScrollLock","parseInt","initialize","SyntaxError","hash$","els$","scrollWidth","clientWidth","patchCodeBlocks","details","open","scrollIntoView","patchDetails","concatMap","script","src","onload","patchScripts","hasAttribute","patchSource","sentinel","patchTables","iif","patchScrollfix","navigation$","toc$","tabs$","search$","protocol","sort","charAt","link","visibility","values","documentElement"],"mappings":"4DACE,SAASA,EAAqBC,GAQ7B,IAPA,IAMIC,EAAUC,EANVC,EAAWH,EAAK,GAChBI,EAAcJ,EAAK,GACnBK,EAAiBL,EAAK,GAIHM,EAAI,EAAGC,EAAW,GACpCD,EAAIH,EAASK,OAAQF,IACzBJ,EAAUC,EAASG,GAChBG,OAAOC,UAAUC,eAAeC,KAAKC,EAAiBX,IAAYW,EAAgBX,IACpFK,EAASO,KAAKD,EAAgBX,GAAS,IAExCW,EAAgBX,GAAW,EAE5B,IAAID,KAAYG,EACZK,OAAOC,UAAUC,eAAeC,KAAKR,EAAaH,KACpDc,EAAQd,GAAYG,EAAYH,IAKlC,IAFGe,GAAqBA,EAAoBhB,GAEtCO,EAASC,QACdD,EAASU,OAATV,GAOD,OAHAW,EAAgBJ,KAAKK,MAAMD,EAAiBb,GAAkB,IAGvDe,IAER,SAASA,IAER,IADA,IAAIC,EACIf,EAAI,EAAGA,EAAIY,EAAgBV,OAAQF,IAAK,CAG/C,IAFA,IAAIgB,EAAiBJ,EAAgBZ,GACjCiB,GAAY,EACRC,EAAI,EAAGA,EAAIF,EAAed,OAAQgB,IAAK,CAC9C,IAAIC,EAAQH,EAAeE,GACG,IAA3BX,EAAgBY,KAAcF,GAAY,GAE3CA,IACFL,EAAgBQ,OAAOpB,IAAK,GAC5Be,EAASM,EAAoBA,EAAoBC,EAAIN,EAAe,KAItE,OAAOD,EAIR,IAAIQ,EAAmB,GAKnBhB,EAAkB,CACrBiB,EAAG,GAGAZ,EAAkB,GAGtB,SAASS,EAAoB1B,GAG5B,GAAG4B,EAAiB5B,GACnB,OAAO4B,EAAiB5B,GAAU8B,QAGnC,IAAIC,EAASH,EAAiB5B,GAAY,CACzCK,EAAGL,EACHgC,GAAG,EACHF,QAAS,IAUV,OANAhB,EAAQd,GAAUW,KAAKoB,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAG/DK,EAAOC,GAAI,EAGJD,EAAOD,QAKfJ,EAAoBO,EAAInB,EAGxBY,EAAoBQ,EAAIN,EAGxBF,EAAoBS,EAAI,SAASL,EAASM,EAAMC,GAC3CX,EAAoBY,EAAER,EAASM,IAClC5B,OAAO+B,eAAeT,EAASM,EAAM,CAAEI,YAAY,EAAMC,IAAKJ,KAKhEX,EAAoBgB,EAAI,SAASZ,GACX,oBAAXa,QAA0BA,OAAOC,aAC1CpC,OAAO+B,eAAeT,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DrC,OAAO+B,eAAeT,EAAS,aAAc,CAAEe,OAAO,KAQvDnB,EAAoBoB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQnB,EAAoBmB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKzC,OAAO0C,OAAO,MAGvB,GAFAxB,EAAoBgB,EAAEO,GACtBzC,OAAO+B,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOnB,EAAoBS,EAAEc,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRvB,EAAoB2B,EAAI,SAAStB,GAChC,IAAIM,EAASN,GAAUA,EAAOiB,WAC7B,WAAwB,OAAOjB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAL,EAAoBS,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRX,EAAoBY,EAAI,SAASgB,EAAQC,GAAY,OAAO/C,OAAOC,UAAUC,eAAeC,KAAK2C,EAAQC,IAGzG7B,EAAoB8B,EAAI,GAExB,IAAIC,EAAaC,OAAqB,aAAIA,OAAqB,cAAK,GAChEC,EAAmBF,EAAW5C,KAAKuC,KAAKK,GAC5CA,EAAW5C,KAAOf,EAClB2D,EAAaA,EAAWG,QACxB,IAAI,IAAIvD,EAAI,EAAGA,EAAIoD,EAAWlD,OAAQF,IAAKP,EAAqB2D,EAAWpD,IAC3E,IAAIU,EAAsB4C,EAM1B,OAFA1C,EAAgBJ,KAAK,CAAC,GAAG,IAElBM,I,yhCCjHF,SAAS0C,IACd,MAAMC,EAAY,IAAIC,EAAA,EAQtB,OAPA,OAAAC,EAAA,GAAUC,SAAU,oBACjBC,KACC,OAAAC,EAAA,GAAMF,WAELG,UAAUN,GAGRA,ECXF,SAASO,EACdC,EAAkBC,EAAmBN,UAErC,OAAOM,EAAKC,cAAiBF,SAAaG,EAarC,SAASC,EACdJ,EAAkBC,EAAmBN,UAErC,MAAMU,EAAKN,EAAcC,EAAUC,GACnC,QAAkB,IAAPI,EACT,MAAM,IAAIC,eACR,8BAA8BN,oBAElC,OAAOK,EAQF,SAASE,IACd,OAAOZ,SAASa,yBAAyBC,YACrCd,SAASa,mBACTL,EAaC,SAASO,EACdV,EAAkBC,EAAmBN,UAErC,OAAOgB,MAAMC,KAAKX,EAAKY,iBAAoBb,IActC,SAASc,EACdC,GAEA,OAAOpB,SAASmB,cAAcC,GASzB,SAASC,EACdC,EAAqBC,GAErBD,EAAOE,YAAYD,G,4BC/Ed,SAASE,EACdf,EAAiB9B,GAAiB,GAE9BA,EACF8B,EAAGgB,QAEHhB,EAAGiB,OAYA,SAASC,EACdlB,GAEA,OAAO,OAAAmB,EAAA,GACL,OAAA9B,EAAA,GAAsBW,EAAI,SAC1B,OAAAX,EAAA,GAAsBW,EAAI,SAEzBT,KACC,OAAA6B,EAAA,GAAI,EAAGC,UAAoB,UAATA,GAClB,OAAAC,EAAA,GAAUtB,IAAOE,MChBhB,SAASqB,EAAiBvB,GAC/B,MAAO,CACLwB,EAAGxB,EAAGyB,WACNC,EAAG1B,EAAG2B,WAaH,SAASC,EACd5B,GAEA,OAAO,OAAAmB,EAAA,GACL,OAAA9B,EAAA,GAAUW,EAAI,UACd,OAAAX,EAAA,GAAUN,OAAQ,WAEjBQ,KACC,OAAA6B,EAAA,GAAI,IAAMG,EAAiBvB,IAC3B,OAAAsB,EAAA,GAAUC,EAAiBvB,KC1C1B,SAAS6B,EACd7B,GAEA,KAAIA,aAAc8B,kBAGhB,MAAM,IAAIC,MAAM,mBAFhB/B,EAAGgC,S,oFCyBP,MAAMC,EAAS,IAAIC,EAAA,EAYbC,EAAY,OAAAC,EAAA,GAAM,IAAM,OAAAC,EAAA,GAC5B,IAAI,IAAeC,IACjB,IAAK,MAAMC,KAASD,EAClBL,EAAOO,KAAKD,OAGfhD,KACC,OAAAkD,EAAA,GAAUC,GAAU,OAAAvB,EAAA,GAAM,OAAAkB,EAAA,GAAGK,GAAS,KACnCnD,KACC,OAAAoD,EAAA,GAAS,IAAMD,EAAOE,gBAG1B,OAAAC,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAmCpC,SAASC,EACdhD,GAEA,OAAOmC,EACJ5C,KACC,OAAA0D,EAAA,GAAIC,GAAYA,EAASC,QAAQnD,IACjC,OAAAyC,EAAA,GAAUS,GAAYjB,EACnB1C,KACC,OAAA6D,EAAA,GAAO,EAAGvC,YAAaA,IAAWb,GAClC,OAAA2C,EAAA,GAAS,IAAMO,EAASG,UAAUrD,IAClC,OAAAoB,EAAA,GAAI,EAAGkC,kBAAkB,CACvBC,MAAQD,EAAYC,MACpBC,OAAQF,EAAYE,YAI1B,OAAAlC,EAAA,GArCC,SAAwBtB,GAC7B,MAAO,CACLuD,MAAQvD,EAAGyD,YACXD,OAAQxD,EAAG0D,cAkCCC,CAAe3D,K,YCvFxB,SAAS4D,EAAwB5D,GACtC,OAAQA,EAAGU,SAGT,IAAK,QACL,IAAK,SACL,IAAK,WACH,OAAO,EAGT,QACE,OAAOV,EAAG6D,mBAWT,SAASC,IACd,OAAO,OAAAzE,EAAA,GAAyBN,OAAQ,WACrCQ,KACC,OAAA6D,EAAA,GAAOW,KAAQA,EAAGC,SAAWD,EAAGE,UAChC,OAAA7C,EAAA,GAAI2C,IAAM,CACR1C,KAAM0C,EAAGvF,IACT,QACEuF,EAAGG,iBACHH,EAAGI,sBAGP,OAAAC,EAAA,M,YClCC,SAASC,EAAYC,GAC1BC,SAASC,KAAOF,EAAIE,KAaf,SAASC,EACdH,EACAI,EAAsBH,UAEtB,OAAOD,EAAIK,OAASD,EAAIC,MACjB,iCAAiCC,KAAKN,EAAIO,UAW5C,SAASC,EACdR,EACAI,EAAsBH,UAEtB,OAAOD,EAAIO,WAAaH,EAAIG,UACrBP,EAAIS,KAAKnJ,OAAS,EAUpB,SAASoJ,IACd,OAAO,IAAIC,EAAA,EAtDJ,IAAIC,IAAIX,SAASC,O,YCInB,SAASW,EACdC,GAAc,UAAEC,IAEhB,OAAOA,EACJ9F,KACC,OAAA+F,EAAA,GAAK,GACL,OAAAlE,EAAA,GAAI,EAAGoD,UAAW,IAAIU,IAAIE,EAAMZ,GAC7Be,WACAC,QAAQ,MAAO,KAElB,OAAA3C,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KCjBtC,SAAS0C,IACd,OAAOlB,SAASQ,KAAKW,UAAU,GAa1B,SAASC,EAAgBZ,GAC9B,MAAM/E,EAAKS,EAAc,KACzBT,EAAGwE,KAAOO,EACV/E,EAAG4F,iBAAiB,QAAS7B,GAAMA,EAAGI,mBACtCnE,EAAG6F,QAUE,SAASC,IACd,OAAO,OAAAzG,EAAA,GAA2BN,OAAQ,cACvCQ,KACC,OAAA6B,EAAA,GAAIqE,GACJ,OAAAnE,EAAA,GAAUmE,KACV,OAAArC,EAAA,GAAO2B,GAAQA,EAAKnJ,OAAS,GAC7B,OAAAwI,EAAA,M,WClCC,SAAS2B,EAAWC,GACzB,MAAMC,EAAQC,WAAWF,GACzB,OAAO,IAAIG,EAAA,EAAoBC,IAC7BH,EAAMI,YAAYtC,GAAMqC,EAAW5D,KAAKuB,EAAGuC,YAE1C/G,KACC,OAAA+B,EAAA,GAAU2E,EAAMK,SAChB,OAAAzD,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KCE7C,MAAMwD,EAA4C,CAChDC,OAAQzG,EAAkB,2BAC1B0G,OAAQ1G,EAAkB,4BAcrB,SAAS2G,EAAUjJ,GACxB,OAAO8I,EAAQ9I,GAAMkJ,QAchB,SAASC,EAAUnJ,EAAcS,GAClCqI,EAAQ9I,GAAMkJ,UAAYzI,GAC5BqI,EAAQ9I,GAAMoI,QAYX,SAASgB,EAAYpJ,GAC1B,MAAMuC,EAAKuG,EAAQ9I,GACnB,OAAO,OAAA4B,EAAA,GAAUW,EAAI,UAClBT,KACC,OAAA6B,EAAA,GAAI,IAAMpB,EAAG2G,SACb,OAAArF,EAAA,GAAUtB,EAAG2G,U,qBC9CZ,SAASG,KACd,MAAO,CACLtF,EAAGuF,KAAKC,IAAI,EAAGC,aACfvF,EAAGqF,KAAKC,IAAI,EAAGE,cASZ,SAASC,IACd,EAAE3F,EAAC,EAAEE,IAEL3C,OAAOqI,SAAS5F,GAAK,EAAGE,GAAK,GClBxB,SAAS2F,KACd,MAAO,CACL9D,MAAQ+D,WACR9D,OAAQ+D,aCwBL,SAASC,KACd,OAAO,OAAAC,EAAA,GAAc,CFCd,OAAAtG,EAAA,GACL,OAAA9B,EAAA,GAAUN,OAAQ,SAAU,CAAE2I,SAAS,IACvC,OAAArI,EAAA,GAAUN,OAAQ,SAAU,CAAE2I,SAAS,KAEtCnI,KACC,OAAA6B,EAAA,GAAI0F,IACJ,OAAAxF,EAAA,GAAUwF,OCpBP,OAAAzH,EAAA,GAAUN,OAAQ,SAAU,CAAE2I,SAAS,IAC3CnI,KACC,OAAA6B,EAAA,GAAIiG,IACJ,OAAA/F,EAAA,GAAU+F,SCcX9H,KACC,OAAA6B,EAAA,GAAI,EAAEuG,EAAQC,MAAU,CAAGD,SAAQC,UACnC,OAAA/E,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAYtC,SAAS8E,GACd7H,GAAiB,QAAE8H,EAAO,UAAEC,IAE5B,MAAMC,EAAQD,EACXxI,KACC,OAAA0I,GAAA,GAAwB,SAItBC,EAAU,OAAAT,EAAA,GAAc,CAACO,EAAOF,IACnCvI,KACC,OAAA6B,EAAA,GAAI,KAAsB,CACxBI,EAAGxB,EAAGmI,WACNzG,EAAG1B,EAAGoI,cAKZ,OAAO,OAAAX,EAAA,GAAc,CAACK,EAASC,EAAWG,IACvC3I,KACC,OAAA6B,EAAA,GAAI,GAAIoC,WAAYmE,SAAQC,SAAUpG,IAAGE,SAAS,CAChDiG,OAAQ,CACNnG,EAAGmG,EAAOnG,EAAIA,EACdE,EAAGiG,EAAOjG,EAAIA,EAAI8B,GAEpBoE,W,sBChCD,SAASS,GACdC,GAAgB,IAAEC,IAIlB,MAAMC,EAAM,OAAAnJ,EAAA,GAAwBiJ,EAAQ,WACzC/I,KACC,OAAA6B,EAAA,GAAI,EAAGhG,UAAWA,IAItB,OAAOmN,EACJhJ,KACC,OAAAkJ,GAAA,GAAS,IAAMD,EAAK,CAAEE,SAAS,EAAMC,UAAU,IAC/C,OAAA1F,EAAA,GAAI2F,GAAWN,EAAOO,YAAYD,IAClC,OAAAE,GAAA,GAAYN,GACZ,OAAApE,EAAA,Q,8BCrCC,SAAS2E,EAASC,GACvB,MAAyB,iBAAXA,GACgB,iBAAhBA,EAAO5D,MACa,iBAApB4D,EAAOC,UACW,iBAAlBD,EAAOvC,OCXvB,SAASyC,EAAYlJ,EAAiBmJ,GAGpC,GAAqB,iBAAVA,GAAuC,iBAAVA,EACtCnJ,EAAGoJ,WAAaD,EAAM5D,gBAGjB,GAAI4D,aAAiBE,KAC1BrJ,EAAGkJ,YAAYC,QAGV,GAAI7I,MAAMgJ,QAAQH,GACvB,IAAK,MAAMvJ,KAAQuJ,EACjBD,EAAYlJ,EAAIJ,GAiBf,SAAS2J,EACdC,EAAaC,KAAkCC,GAE/C,MAAM1J,EAAKV,SAASmB,cAAc+I,GAGlC,GAAIC,EACF,IAAK,MAAME,KAAQ9N,OAAO+N,KAAKH,GACG,kBAArBA,EAAWE,GACpB3J,EAAG6J,aAAaF,EAAMF,EAAWE,IAC1BF,EAAWE,IAClB3J,EAAG6J,aAAaF,EAAM,IAG5B,IAAK,MAAMR,KAASO,EAClBR,EAAYlJ,EAAImJ,GAGlB,OAAOnJ,E,kQC9DF,SAAS8J,EACdtL,EAAauL,GAEb,OAAO,OAAA3H,EAAA,GAAM,KACX,MAAMhH,EAAO4O,eAAeC,QAAQzL,GACpC,GAAIpD,EACF,OAAO,OAAAiH,EAAA,GAAG6H,KAAKC,MAAM/O,IAGhB,CACL,MAAMgP,EAASL,IAUf,OATAK,EAAO3K,UAAUvB,IACf,IACE8L,eAAeK,QAAQ7L,EAAK0L,KAAKI,UAAUpM,IAC3C,MAAOqM,OAMJH,K,WCXb,IAAII,EAcG,SAASC,EACdjM,EAAmBN,GAEnB,QAAoB,IAATsM,EAAsB,CAC/B,MAAMxK,EAAK,YAAkB,WAC7BwK,EAAON,KAAKC,MAAMnK,EAAG0K,aAEvB,QAAyB,IAAdF,EAAKhM,GACd,MAAM,IAAIyB,eAAe,wBAAwBzB,GAEnD,YAAwB,IAAVN,EACVsM,EAAKhM,GAAKgH,QAAQ,IAAKtH,EAAMqH,YAC7BiF,EAAKhM,GAgBJ,SAASmM,EAASzM,EAAeQ,GACtC,IAAIhD,EAAIgD,EACR,GAAIR,EAAMtC,OAASF,EAAG,CACpB,KAAoB,MAAbwC,EAAMxC,MAAgBA,EAAI,IACjC,OAAUwC,EAAMwH,UAAU,EAAGhK,GAAtB,MAET,OAAOwC,EAmBF,SAAS0M,EAAM1M,GACpB,GAAIA,EAAQ,IAAK,CAEf,QAAYA,EAAQ,MAAY,KAAM2M,WADpB3M,EAAQ,KAAO,IAAO,KACjC,IAEP,OAAOA,EAAMqH,WAaV,SAASR,EAAK7G,GACnB,IAAIqL,EAAI,EACR,IAAK,IAAI7N,EAAI,EAAGoP,EAAM5M,EAAMtC,OAAQF,EAAIoP,EAAKpP,IAC3C6N,GAAOA,GAAK,GAAKA,EAAKrL,EAAM6M,WAAWrP,GACvC6N,GAAK,EAEP,OAAOA,I,kCC/IT,41B,maCwDO,SAASyB,GACd,UAAE7L,EAAS,QAAE8L,IAEb,IAAK,gBACH,OAAO,IAGT9L,EAAUM,UAAU,KACH,YAAY,cACpByL,QAAQ,CAACC,EAAOC,KACrB,MAAMC,EAASF,EAAMG,cACrBD,EAAOE,GAAK,UAAUH,EACtBC,EAAOG,aACL,YAAsBH,EAAOE,IAC7BJ,OAMN,MAAMM,EAAa,IAAItF,EAAA,EAA8BC,IACnD,IAAI,EAAY,iBAAiBsF,GAAG,UAAW3H,GAAMqC,EAAW5D,KAAKuB,MAEpExE,KACC,OAAA6E,EAAA,MAYJ,OARAqH,EACGlM,KACC,OAAA0D,EAAA,GAAIc,GAAMA,EAAG4H,kBACb,OAAAnM,EAAA,GAAM,YAAU,sBAEfC,UAAUwL,GAGRQ,E,oECrCF,SAASG,GACd,SAAEC,GAA2B,IAE7B,MAAMZ,EAAU,IAAI/I,EAAA,EAGd4J,EAAS,YAAc,OA4B7B,OA3BAA,EAAOC,UAAUC,IAAI,YAAa,cAGlCf,EACG1L,KACC,OAAAkD,EAAA,GAAUwJ,GAAQ,OAAA5J,EAAA,GAAG/C,SAAS4M,MAC3B3M,KACC,OAAA6B,EAAA,GAAI+K,GAAaA,EAAUjD,YAAY4C,IACvC,OAAAM,EAAA,GAAUC,EAAA,GACV,OAAAC,EAAA,GAAM,GACN,OAAArJ,EAAA,GAAIjD,IACFA,EAAGoJ,UAAY6C,EACfjM,EAAG6J,aAAa,gBAAiB,UAEnC,OAAAyC,EAAA,GAAMT,GAAY,KAClB,OAAA5I,EAAA,GAAIjD,GAAMA,EAAGuM,gBAAgB,kBAC7B,OAAAD,EAAA,GAAM,KACN,OAAArJ,EAAA,GAAIjD,IACFA,EAAGoJ,UAAY,GACfpJ,EAAGwM,cAKR/M,UAAUgN,EAAA,GAGRxB,E,mGCUF,SAASyB,EACdC,GAAgB,UAAExN,EAAS,UAAE4I,EAAS,UAAE1C,IAIpC,sBAAuBuH,UACzBA,QAAQC,kBAAoB,UAG9B,OAAAxN,EAAA,GAAUN,OAAQ,gBACfU,UAAU,KACTmN,QAAQC,kBAAoB,SAIhC,MAAMC,EAAU,YAA4B,kCACrB,IAAZA,IACTA,EAAQtI,KAAOsI,EAAQtI,MAGzB,MAAMuI,EAAS,OAAA1N,EAAA,GAAsBC,SAAS4M,KAAM,SACjD3M,KACC,OAAA6D,EAAA,GAAOW,KAAQA,EAAGC,SAAWD,EAAGE,UAChC,OAAAxB,EAAA,GAAUsB,IACR,GAAIA,EAAGlD,kBAAkBT,YAAa,CACpC,MAAMJ,EAAK+D,EAAGlD,OAAOmM,QAAQ,KAC7B,GACEhN,IAAOA,EAAGa,QACV,YAAgBb,IAChB2M,EAAKM,SAASjN,EAAGwE,MAIjB,OAFK,YAAiBxE,IACpB+D,EAAGG,iBACE,OAAA7B,EAAA,GAAGrC,GAGd,OAAO,MAET,OAAAoB,EAAA,GAAIpB,IAAM,CAAGsE,IAAK,IAAIY,IAAIlF,EAAGwE,SAC7B,OAAAJ,EAAA,MAIJ2I,EAAOtN,UAAU,KACf,YAAU,UAAU,KAItB,MAAMyN,EAAQH,EACXxN,KACC,OAAA6D,EAAA,GAAO,EAAGkB,UAAW,YAAiBA,IACtC,OAAAF,EAAA,MAIE+I,EAAO,OAAA9N,EAAA,GAAyBN,OAAQ,YAC3CQ,KACC,OAAA6D,EAAA,GAAOW,GAAmB,OAAbA,EAAGqJ,OAChB,OAAAhM,EAAA,GAAI2C,IAAM,CACRO,IAAK,IAAIY,IAAIX,SAASC,MACtBmD,OAAQ5D,EAAGqJ,SAEb,OAAAhJ,EAAA,MAIJ,OAAAjD,EAAA,GAAM+L,EAAOC,GACV5N,KACC,OAAA8N,EAAA,GAAqB,CAACC,EAAM9K,IAAS8K,EAAKhJ,IAAIE,OAAShC,EAAK8B,IAAIE,MAChE,OAAApD,EAAA,GAAI,EAAGkD,SAAUA,IAEhB7E,UAAU4F,GAGf,MAAMkI,EAAQlI,EACX9F,KACC,OAAA0I,EAAA,GAAwB,YACxB,OAAAuF,EAAA,GAAK,GACL,OAAA/K,EAAA,GAAU6B,GAAO,OAAA/D,EAAA,GAAKkN,MAAMnJ,EAAIE,KAAM,CACpCkJ,YAAa,gBACZC,KAAKC,GAAOA,EAAI3B,SAChB1M,KACC,OAAAsO,EAAA,GAAW,KACT,YAAYvJ,GACL,QAIb,OAAAF,EAAA,MAIJ8I,EACG3N,KACC,OAAAuO,EAAA,GAAOP,IAEN9N,UAAU,EAAG6E,UACZsI,QAAQmB,UAAU,GAAI,GAAIzJ,EAAIiB,cAIpC,MAAMyI,EAAM,IAAIC,UAChBV,EACGhO,KACC,OAAA6B,EAAA,GAAI8M,GAAYF,EAAIG,gBAAgBD,EAAU,eAE7CzO,UAAUN,GAGf,MAAMiP,EAAW,OAAAjN,EAAA,GAAM+L,EAAOC,GAC3B5N,KACC,OAAAuO,EAAA,GAAO3O,IAIXiP,EAAS3O,UAAU,EAAG6E,MAAKqD,aACrBrD,EAAIS,OAAS4C,EACf,YAAgBrD,EAAIS,MAEpB,YAAkB4C,GAAU,CAAEjG,EAAG,MAKrC0M,EACG7O,KACC,OAAA8O,EAAA,GAAelP,IAEdM,UAAU,EAAE,EAAI6O,QAAOC,YACtBjP,SAASgP,MAAQA,EAGjB,IAAK,MAAM3O,IAAY,CACrB,wBACA,sBACA,4BACC,CACD,MAAM6C,EAAO,YAAW7C,EAAU4O,GAC5BjB,EAAO,YAAW3N,EAAUL,SAASiP,WAEzB,IAAT/L,QACS,IAAT8K,GAEP,YAAeA,EAAM9K,GAKzBlD,SAASkP,cAAc,IAAIC,YAAY,uBAI7C1G,EACGxI,KACC,OAAAmP,EAAA,GAAa,KACb,OAAAzG,EAAA,GAAwB,WAEvBxI,UAAU,EAAGkI,aACZiF,QAAQ+B,aAAahH,EAAQ,MAInC,OAAAxG,EAAA,GAAM4L,EAAQI,GACX5N,KACC,OAAAqP,EAAA,GAAY,EAAG,GACf,OAAAxL,EAAA,GAAO,EAAEkK,EAAM9K,KACN8K,EAAKhJ,IAAIO,WAAarC,EAAK8B,IAAIO,WAC9B,YAAiBrC,EAAK8B,MAEhC,OAAAlD,EAAA,GAAI,EAAE,CAAEgM,KAAWA,IAElB3N,UAAU,EAAGkI,aACZ,YAAkBA,GAAU,CAAEjG,EAAG,M,WCxLlC,SAASmN,IACd,MAAMC,EAAY,cACfvP,KACC,OAAA6B,EAAA,GAAmB5C,GAAQ,OAAD,QACxBJ,KAAM,YAAU,UAAY,SAAW,UACpCI,IAEL,OAAA4E,EAAA,GAAO,EAAGhF,WACR,GAAa,WAATA,EAAmB,CACrB,MAAM2Q,EAAS,cACf,QAAsB,IAAXA,EACT,OAAQ,YAAwBA,GAEpC,OAAO,IAET,OAAA3K,EAAA,MA+FJ,OA3FA0K,EACGvP,KACC,OAAA6D,EAAA,GAAO,EAAGhF,UAAoB,WAATA,GACrB,OAAAiQ,EAAA,GACE,uBAAa,gBACb,uBAAa,mBAGd5O,UAAU,EAAEjB,EAAKwH,EAAOvJ,MACvB,MAAMsS,EAAS,cACf,OAAQvQ,EAAI6C,MAGV,IAAK,QACC0N,IAAW/I,GACbxH,EAAIwQ,QACN,MAGF,IAAK,SACL,IAAK,MACH,YAAU,UAAU,GACpB,YAAgBhJ,GAAO,GACvB,MAGF,IAAK,UACL,IAAK,YACH,QAAsB,IAAX+I,EACT,YAAgB/I,OACX,CACL,MAAMiJ,EAAM,CAACjJ,KAAU,YACrB,wDACAvJ,IAEIf,EAAIqL,KAAKC,IAAI,GACjBD,KAAKC,IAAI,EAAGiI,EAAIC,QAAQH,IAAWE,EAAIrT,QACxB,YAAb4C,EAAI6C,MAAsB,EAAI,IAE9B4N,EAAIrT,QACR,YAAgBqT,EAAIvT,IAItB8C,EAAIwQ,QACJ,MAGF,QACMhJ,IAAU,eACZ,YAAgBA,MAK5B8I,EACGvP,KACC,OAAA6D,EAAA,GAAO,EAAGhF,UAAoB,WAATA,GACrB,OAAAiQ,EAAA,GAAe,uBAAa,kBAE3B5O,UAAU,EAAEjB,EAAKwH,MAChB,OAAQxH,EAAI6C,MAGV,IAAK,IACL,IAAK,IACL,IAAK,IACH,YAAgB2E,GAChB,YAAoBA,GACpBxH,EAAIwQ,QACJ,MAGF,IAAK,IACL,IAAK,IACH,MAAM1B,EAAO,YAAW,yBACJ,IAATA,GACTA,EAAKzH,QACP,MAGF,IAAK,IACL,IAAK,IACH,MAAMrD,EAAO,YAAW,yBACJ,IAATA,GACTA,EAAKqD,WAMViJ,E,8CCrMT,uIAgFA,IAAIK,EAeG,SAASC,EACdC,GAAoB,UAAElQ,IAEtBgQ,EAAchQ,EACXI,KAGC,YAAID,GAAY+P,EAAMC,OAAqB,CAACC,EAAY9R,KACtD,MAAMuC,EAAK,YAAW,sBAAsBvC,KAAS6B,GACrD,OAAO,OAAP,wBACKiQ,QACc,IAAPvP,EAAqB,CAAE,CAACvC,GAAOuC,GAAO,KAEjD,KAGH,YAAK,CAACsN,EAAM9K,KACV,IAAK,MAAM/E,KAAQ4R,EACjB,OAAQ5R,GAGN,IAAK,WACL,IAAK,eACL,IAAK,YACL,IAAK,OACCA,KAAQ6P,QAA8B,IAAfA,EAAK7P,KAC9B,YAAe6P,EAAK7P,GAAQ+E,EAAK/E,IACjC6P,EAAK7P,GAAQ+E,EAAK/E,IAEpB,MAGF,aAC4B,IAAf+E,EAAK/E,GACd6P,EAAK7P,GAAQ,YAAW,sBAAsBA,aAEvC6P,EAAK7P,GAGpB,OAAO6P,IAIT,YAAY,CAAExK,WAAY,EAAGC,UAAU,KAsBtC,SAASyM,EACd/R,GAEA,OAAO0R,EACJ5P,KACC,YAAUgQ,QACoB,IAArBA,EAAW9R,GACd,YAAG8R,EAAW9R,IACd,KAEN,iB,gCC1IC,SAASgS,EACdzP,EAAiB9B,GAEjB8B,EAAG6J,aAAa,gBAAiB3L,EAAQ,OAAS,IAQ7C,SAASwR,EACd1P,GAEAA,EAAGuM,gBAAgB,iBAWd,SAASoD,EACd3P,EAAiB9B,GAEjB8B,EAAG+L,UAAU6D,OAAO,uBAAwB1R,GAQvC,SAAS2R,EACd7P,GAEAA,EAAG+L,UAAUS,OAAO,wBAvEtB,yI,gCCAA,gW,yKCoCWsD,E,OCDJ,SAASC,EAAsBxE,GACpC,OACE,WADK,CACL,UACEyE,MAAM,uBACN1B,MAAO,YAAU,kBAAiB,wBACX,IAAI/C,aDajC,SAAS0E,EACP3Q,EAA2C4Q,GAE3C,MAAM7E,EAAS6E,EAAOJ,EAAKK,OACrBC,EAASF,EAAOJ,EAAKO,OAGrBC,EAAUzU,OAAO+N,KAAKtK,EAASiR,OAClCnN,OAAO5E,IAAQc,EAASiR,MAAM/R,IAC9B4C,IAAI5C,GAAO,CAAC,uBAAMA,GAAY,MAC9BgS,OACAvR,MAAM,GAAI,GAGPqF,EAAMhF,EAASiF,SACrB,OACE,WADK,CACL,KAAGC,KAAMF,EAAK0L,MAAM,yBAAyBS,UAAW,GACtD,uBACET,MAAO,CAAC,+BAAgC3E,EACpC,CAAC,uCACD,IACFqF,KAAK,KAAI,gBACIpR,EAASqR,MAAM9F,QAAQ,IAErCQ,EAAS,GAAK,mBAAK2E,MAAM,mCAC1B,kBAAIA,MAAM,2BAA2B1Q,EAASgP,OAC7C8B,EAAS,GAAK9Q,EAAS2M,KAAKrQ,OAAS,GACpC,iBAAGoU,MAAM,4BACN,YAAS1Q,EAAS2M,KAAM,MAG5BmE,EAAS,GAAKE,EAAQ1U,OAAS,GAC9B,iBAAGoU,MAAM,2BACN,YAAU,8B,KAAoCM,KAoBpD,SAASM,EACdnU,EAAsBoU,EAAoBC,KAE1C,MAAMC,EAAO,IAAItU,GAGX4O,EAAS0F,EAAKC,UAAUC,IAAQA,EAAI1M,SAAS0I,SAAS,OACrDiE,GAAWH,EAAKjU,OAAOuO,EAAQ,GAGtC,IAAID,EAAQ2F,EAAKC,UAAUC,GAAOA,EAAIN,MAAQE,IAC/B,IAAXzF,IACFA,EAAQ2F,EAAKnV,QAGf,MAAMuV,EAAOJ,EAAK9R,MAAM,EAAGmM,GACrBgG,EAAOL,EAAK9R,MAAMmM,GAGlB1B,EAAW,CACfuG,EAAqBiB,EAASpB,EAAKK,UAAY9E,GAAoB,IAAVD,OACtD+F,EAAK/P,IAAIiQ,GAAWpB,EAAqBoB,EAASvB,EAAKO,YACvDe,EAAKxV,OAAS,CACf,uBAASoU,MAAM,0BACb,uBAASS,UAAW,GACjBW,EAAKxV,OAAS,GAAqB,IAAhBwV,EAAKxV,OACrB,YAAU,0BACV,YAAU,2BAA4BwV,EAAKxV,SAG7CwV,EAAKhQ,IAAIiQ,GAAWpB,EAAqBoB,EAASvB,EAAKO,WAE3D,IAIN,OACE,WADK,CACL,MAAIL,MAAM,0BACPtG,GE5GA,SAAS4H,EACdC,GAEA,OACE,WADK,CACL,MAAIvB,MAAM,oBACPuB,EAAMnQ,IAAIoQ,GACT,WADiB,CACjB,MAAIxB,MAAM,mBAAmBwB,KCP9B,SAASC,EACdC,GAEA,OACE,WADK,CACL,OAAK1B,MAAM,0BACT,mBAAKA,MAAM,qBACR0B,KHLT,SAAW5B,GACT,uBACA,uBAFF,CAAWA,MAAI,M,+BIJR,SAAS6B,EACd3R,EAAiB9B,GAEjB8B,EAAG4R,MAAMC,IAAS3T,EAAH,KAQV,SAAS4T,EACd9R,GAEAA,EAAG4R,MAAMC,IAAM,GAWV,SAASE,EACd/R,EAAiB9B,GAEjB8B,EAAG4R,MAAMpO,OAAYtF,EAAH,KAQb,SAAS8T,EACdhS,GAEAA,EAAG4R,MAAMpO,OAAS,GAvEpB,yI,mCCAA,uT,+OC4DO,SAASyO,EAAiBjM,GAC/B,OAAOA,EACJkM,MAAM,cACJ9Q,IAAI,CAACmP,EAAOnF,IAAkB,EAARA,EACnBmF,EAAM/K,QAAQ,+BAAgC,MAC9C+K,GAEHG,KAAK,IACPlL,QAAQ,kCAAmC,IAC3C2M,O,ICtCaC,E,8DA2EX,SAASC,EACdzJ,GAEA,OAAOA,EAAQvH,OAAS+Q,EAAkBE,MAUrC,SAASC,EACd3J,GAEA,OAAOA,EAAQvH,OAAS+Q,EAAkBI,MAUrC,SAASC,EACd7J,GAEA,OAAOA,EAAQvH,OAAS+Q,EAAkBM,OCvE5C,SAASC,GACP,OAAE3J,EAAM,KAAE+H,EAAI,MAAE3F,IAIW,IAAvBpC,EAAOwB,KAAK5O,QAAmC,OAAnBoN,EAAOwB,KAAK,KAC1CxB,EAAOwB,KAAO,CAAC,YAAU,wBAGF,cAArBxB,EAAO4J,YACT5J,EAAO4J,UAAY,YAAU,4BAQ/B,MAAO,CAAE5J,SAAQ+H,OAAM3F,QAAOyH,SALb,YAAU,0BACxBX,MAAM,WACN9O,OAAO0P,UAsBL,SAASC,EACdzO,GAAa,OAAE0O,EAAM,MAAEC,IAEvB,MAAM3K,EAAS,IAAI4K,OAAO5O,GAGpBiE,EAAM,IAAIrG,EAAA,EACVsG,EAAM,YAAYF,EAAQ,CAAEC,QAC/BhJ,KACC,OAAA8O,EAAA,GAAe4E,GACf,OAAA7R,EAAA,GAAI,EAAEwH,EAASxD,MACb,GAAIqN,EAAsB7J,GACxB,IAAK,MAAMnM,KAAUmM,EAAQxN,KAC3B,IAAK,MAAMkE,KAAY7C,EACrB6C,EAASiF,SAAW,GAAGa,KAAQ9F,EAASiF,WAE9C,OAAOqE,IAET,OAAAxE,EAAA,MAeJ,OAXA4O,EACGzT,KACC,OAAA6B,EAAA,GAAqChG,IAAQ,CAC3CiG,KAAM+Q,EAAkBe,MACxB/X,KAAMuX,EAAiBvX,MAEzB,OAAAgR,EAAA,GAAU,MAET3M,UAAU8I,EAAI/F,KAAK/D,KAAK8J,IAGtB,CAAEA,MAAKC,QDvGhB,SAAkB4J,GAChB,qBACA,qBACA,qBACA,uBAJF,CAAkBA,MAAiB,M,yCE/BnC,8EAqFO,SAASgB,GACd,QAAEtL,EAAO,MAAEuL,EAAK,UAAEtL,EAAS,QAAEuL,IAE7B,OAAO,YACL,YAAUtT,GAAMsT,EACb/T,KACC,YAAUgU,GAGJA,EACK,uBAAavT,EAAI,CAAEqT,QAAOtL,cAC9BxI,KACC,uBAAaS,EAAI,CAAE8H,YACnB,YAAI0L,IAAW,CAAGA,cAKf,YAAG,U,6BCvGtB,gd,6CCAA,wJAsFO,SAASC,EACdzT,GAAiB,MAAEqT,EAAK,UAAEtL,IAE1B,MAAM2L,EAAS1T,EAAGsL,cAAelD,UAClBpI,EAAGsL,cAAeA,cAAelD,UAGhD,OAAO,YAAc,CAACiL,EAAOtL,IAC1BxI,KACC,YAAI,GAAIoI,SAAQnE,WAAYmE,QAAUjG,UAI7B,CACL8B,OAJFA,EAASA,EACLuD,KAAK4M,IAAID,EAAQ3M,KAAKC,IAAI,EAAGtF,EAAIiG,IACjC+L,EAGFE,KAAMlS,GAAKiG,EAAS+L,KAGxB,YAA8B,CAACG,EAAGC,IACzBD,EAAErQ,SAAWsQ,EAAEtQ,QACfqQ,EAAED,OAAWE,EAAEF,OAevB,SAASG,EACd/T,GAAiB,QAAE8H,IAEnB,OAAO,YAGL,YAAU,KACV,YAAeA,GACf,YAAI,GAAItE,SAAQoQ,SAAUpQ,OAAQmE,OAChC,YAAiB3H,EAAIwD,GAGjBoQ,EACF,YAAiB5T,EAAI2H,GAErB,YAAmB3H,KAIvB,YAAI,EAAEwT,KAAaA,GAGnB,YAAS,KACP,YAAmBxT,GACnB,YAAmBA,Q,6BCjJzB,0E,6BCAA,qGAiGO,SAASgU,GACd,QAAElM,EAAO,MAAEuL,EAAK,UAAEtL,EAAS,QAAEkM,IAE7B,OAAO,YACL,YAAUjU,GAAMiU,EACb1U,KACC,YAAU2U,IAGR,GAAIA,EAAQ,CACV,MAAMjF,EAAM,YAA+B,gBAAiBjP,GAGtDmU,EAAW,uBAAanU,EAAI,CAAEqT,QAAOtL,cACxCxI,KACC,uBAAaS,EAAI,CAAE8H,aAIjBsM,EAAW,0BAAgBnF,EAAK,CAAEnH,UAASC,cAC9CxI,KACC,0BAAgB0P,IAIpB,OAAO,YAAc,CAACkF,EAAUC,IAC7B7U,KACC,YAAI,EAAEiU,EAASa,MAAa,CAAGb,UAASa,cAK5C,OAAO,YAAG,W,6CCjItB,+LAyFO,SAASC,EACdrF,GAA0B,QAAEnH,EAAO,UAAEC,IAErC,MAAM2J,EAAQ,IAAI6C,IAClB,IAAK,MAAMvU,KAAMiP,EAAK,CACpB,MAAM1D,EAAKiJ,mBAAmBxU,EAAG+E,KAAKW,UAAU,IAC1C7E,EAAS,YAAW,QAAQ0K,YACZ,IAAX1K,GACT6Q,EAAM+C,IAAIzU,EAAIa,GAIlB,MAAM6T,EAAU5M,EACbvI,KACC,YAAIoV,GAAU,GAAKA,EAAOnR,SAyE9B,OArEmB,YAAiBlE,SAAS4M,MAC1C3M,KACC,YAAwB,UAGxB,YAAI,KACF,IAAIqV,EAA4B,GAChC,MAAO,IAAIlD,GAAOpC,OAAO,CAAClE,GAAQyJ,EAAQhU,MACxC,KAAO+T,EAAKhZ,QAAQ,CAElB,KADa8V,EAAM5T,IAAI8W,EAAKA,EAAKhZ,OAAS,IACjC8E,SAAWG,EAAOH,SAGzB,MAFAkU,EAAKE,MAOT,IAAInN,EAAS9G,EAAOuH,UACpB,MAAQT,GAAU9G,EAAOyK,eAEvB3D,GADA9G,EAASA,EAAOyK,eACAlD,UAIlB,OAAOgD,EAAMqJ,IACX,IAAIG,EAAO,IAAIA,EAAMC,IAASE,UAC9BpN,IAED,IAAI4M,OAIT,YAAUnJ,GAAS,YAAc,CAACsJ,EAAS3M,IACxCxI,KACC,YAAK,EAAE+N,EAAM9K,IAAQkR,GAAU/L,QAAUjG,UAGvC,KAAOc,EAAK5G,QAAQ,CAClB,MAAO,CAAE+L,GAAUnF,EAAK,GACxB,KAAImF,EAAS+L,EAAShS,GAGpB,MAFA4L,EAAO,IAAIA,EAAM9K,EAAKnG,SAO1B,KAAOiR,EAAK1R,QAAQ,CAClB,MAAO,CAAE+L,GAAU2F,EAAKA,EAAK1R,OAAS,GACtC,KAAI+L,EAAS+L,GAAUhS,GAGrB,MAFAc,EAAO,CAAC8K,EAAKwH,SAAWtS,GAO5B,MAAO,CAAC8K,EAAM9K,IACb,CAAC,GAAI,IAAI4I,KACZ,YAAqB,CAACyI,EAAGC,IAChBD,EAAE,KAAOC,EAAE,IACXD,EAAE,KAAOC,EAAE,OAQzBvU,KACC,YAAI,EAAE+N,EAAM9K,MAAU,CACpB8K,KAAMA,EAAKlM,IAAI,EAAEwT,KAAUA,GAC3BpS,KAAMA,EAAKpB,IAAI,EAAEwT,KAAUA,MAI7B,YAAU,CAAEtH,KAAM,GAAI9K,KAAM,KAC5B,YAAY,EAAG,GACf,YAAI,EAAEqR,EAAGC,KAGHD,EAAEvG,KAAK1R,OAASkY,EAAExG,KAAK1R,OAClB,CACL0R,KAAMwG,EAAExG,KAAKrO,MAAM8H,KAAKC,IAAI,EAAG6M,EAAEvG,KAAK1R,OAAS,GAAIkY,EAAExG,KAAK1R,QAC1D4G,KAAM,IAKD,CACL8K,KAAMwG,EAAExG,KAAKrO,OAAO,GACpBuD,KAAMsR,EAAEtR,KAAKvD,MAAM,EAAG6U,EAAEtR,KAAK5G,OAASiY,EAAErR,KAAK5G,WAgBlD,SAASoZ,EACd/F,GAEA,OAAO,YAGL,YAAU,KACV,YAAI,EAAG3B,OAAM9K,WAGX,IAAK,MAAOxC,KAAOwC,EACjB,YAAkBxC,GAClB,YAAgBA,GAIlBsN,EAAKpC,QAAQ,EAAElL,GAAKoL,KAClB,YAAgBpL,EAAIoL,IAAUkC,EAAK1R,OAAS,GAC5C,YAAcoE,GAAI,OAKtB,YAAS,KACP,IAAK,MAAMA,KAAMiP,EACf,YAAkBjP,GAClB,YAAgBA,Q,yPCvJjB,SAASiV,GACd,IAAEzM,EAAG,IAAED,IACP,OAAE2M,EAAM,OAAEC,EAAM,QAAEC,IAElB,OAAO,OAAA7V,EAAA,GACL,OAAAkD,EAAA,GAAU,KAGR,MAAM4S,EAAU7M,EACbjJ,KACC,OAAA6D,EAAA,GAAO,KACP,OAAA5D,EAAA,GAAoB,SACpB,OAAA8B,EAAA,GAAU,YAad,OATAiH,EACGhJ,KACC,OAAA6D,EAAA,GAAO,KACP,OAAA0K,EAAA,GAAOuH,GACP,OAAA/P,EAAA,GAAK,IAEJ7F,UAAU8I,EAAI/F,KAAK/D,KAAK8J,IAGtB,OAAAd,EAAA,GAAc,CAAC4N,EAASH,EAAQE,EAASD,IAC7C5V,KACC,OAAA6B,EAAA,GAAI,EAAEkU,EAAQtP,EAAOvJ,MAAY,CAC/B6Y,SACAtP,QACAvJ,gB,2DC9CL,SAAS8Y,GACd,IAAEhN,GAAqCiN,EAAwB,IAE/D,OAAO,OAAAjW,EAAA,GACL,OAAAkD,EAAA,GAAUzC,IACR,MAAMkV,EClBL,SACLlV,GAAsB,UAAEyV,GAA4B,IAEpD,MAAMC,EAAKD,GAAa,IAGlBrL,EAAS,OAAAjJ,EAAA,GACb,OAAA9B,EAAA,GAAUW,EAAI,SACd,OAAAX,EAAA,GAAUW,EAAI,SAAST,KAAK,OAAA+M,EAAA,GAAM,KAEjC/M,KACC,OAAA6B,EAAA,GAAI,IAAMsU,EAAG1V,EAAG9B,QAChB,OAAAoD,EAAA,GAAUoU,EAAG1V,EAAG9B,QAChB,OAAAmP,EAAA,MAIEsI,EAAS,YAAkB3V,GAGjC,OAAO,OAAAyH,EAAA,GAAc,CAAC2C,EAAQuL,IAC3BpW,KACC,OAAA6B,EAAA,GAAI,EAAElD,EAAO8C,MAAW,CAAG9C,QAAO8C,YDJnB4U,CAAiB5V,EAAIwV,GAwBpC,OArBAN,EACG3V,KACC,OAAA0I,EAAA,GAAwB,SACxB,OAAA7G,EAAA,GAAI,EAAGlD,YAAgC,CACrCmD,KAAM,IAAkBmR,MACxBpX,KAAM8C,MAGPuB,UAAU8I,EAAI/F,KAAK/D,KAAK8J,IAG7B2M,EACG3V,KACC,OAAA0I,EAAA,GAAwB,UAEvBxI,UAAU,EAAGuB,YACRA,GACF,YAAU,SAAUA,KAIrBkU,K,4BE1DN,SAASW,IACd,OAAO,OAAAtW,EAAA,GACL,OAAAkD,EAAA,GAAUzC,GCXP,SACLA,GAEA,OAAO,OAAAX,EAAA,GAAUW,EAAI,SAClBT,KACC,OAAAC,EAAA,QAAMM,IDMQgW,CAAiB9V,GAC9BT,KACC,OAAAuJ,EAAA,GAAY,YAAa,iBACzB,OAAA7F,EAAA,GAAI,KACJ,OAAAzD,EAAA,QAAMM,KAGV,OAAAwB,EAAA,QAAUxB,I,2DEoBP,SAASiW,EACd/V,EAAiBmJ,GAEjBnJ,EAAGkJ,YAAYC,GCEV,SAAS6M,EACdhW,GAAiB,OAAEkV,EAAM,OAAEe,EAAM,OAAEC,IAEnC,MAAMC,EAAO,YAAkB,0BAA2BnW,GACpDoW,EAAO,YAAkB,0BAA2BpW,GAC1D,OAAO,OAAAT,EAAA,GAGL,OAAA8O,EAAA,GAAe6G,EAAQe,GACvB,OAAA7U,EAAA,GAAI,EAAE3E,EAAQuJ,MACRA,EAAM9H,MDvDT,SACL8B,EAAiB9B,GAEjB,OAAQA,GAGN,KAAK,EACH8B,EAAG0K,YAAc,YAAU,sBAC3B,MAGF,KAAK,EACH1K,EAAG0K,YAAc,YAAU,qBAC3B,MAGF,QACE1K,EAAG0K,YAAc,YAAU,sBAAuBxM,ICuChDmY,CAAoBD,EAAM3Z,EAAOb,QD9BlC,SACLoE,GAEAA,EAAG0K,YAAc,YAAU,6BC6BrB4L,CAAsBF,GAEjB3Z,IAIT,OAAAgG,EAAA,GAAUhG,IACR,MAAM8Z,EAAa,IAAI9Z,EAAO2E,IAAI,EAAE+P,KAAUA,EAAKR,OAAQ,GAC3D,OAAOuF,EACJ3W,KAGC,OAAA6M,EAAA,GAAUC,EAAA,GACV,OAAAmK,EAAA,GAAKpL,IACH,MAAMe,EAAYnM,EAAGsL,cACrB,KAAOF,EAAQ3O,EAAOb,SACpBma,EAAsBI,EAAM,YAC1B1Z,EAAO2O,KAAUmL,EAAWnL,OAE1Be,EAAUsK,aAAetK,EAAUzI,aAAe,OAGxD,OAAO0H,GACN,GAGH,OAAA5L,EAAA,GAAM/C,GAGN,OAAAkG,EAAA,GAAS,MDpCZ,SACL3C,GAEAA,EAAGoJ,UAAY,GCkCLsN,CAAsBP,SCxD3B,SAASQ,GACd,IAAEnO,IAAqC,OAAE0M,IAEzC,OAAO,OAAA3V,EAAA,GACL,OAAAkD,EAAA,GAAUzC,IACR,MAAMmM,EAAYnM,EAAGsL,cAGf2K,EAASzN,EACZjJ,KACC,OAAA6D,EAAA,GAAO,KACP,OAAA5D,EAAA,IAAM,IAIJ0W,EAAS,YAAmB/J,GAC/B5M,KACC,OAAA6B,EAAA,GAAI,EAAGM,OACEA,GAAKyK,EAAUsK,aAAetK,EAAUzI,aAAe,IAEhE,OAAA2J,EAAA,KACA,OAAAjK,EAAA,GAAO0P,UAIX,OAAOtK,EACJjJ,KACC,OAAA6D,EAAA,GAAO,KACP,OAAAhC,EAAA,GAAI,EAAGhG,UAAWA,GAClB4a,EAAkBhW,EAAI,CAAEkV,SAAQe,SAAQC,WACxC,OAAA5U,EAAA,GAAU,U,kMCPb,SAASsV,GACd,UAAEzX,EAAS,UAAE4I,IAEb,OAAO,OAAAxI,EAAA,GACL,OAAAkD,EAAA,GAAUzC,IACR,MAAM8H,ECzBL,SACL9H,GAAiB,UAAEb,IAEnB,OAAOA,EACJI,KACC,OAAA6B,EAAA,GAAI,KACF,MAAMyV,EAASC,iBAAiB9W,GAChC,MAAO,CACL,SACA,kBACAiN,SAAS4J,EAAOE,YAEpB,OAAA1J,EAAA,KACA,OAAA5K,EAAA,GAAUuU,GACJA,EACK,YAAiBhX,GACrBT,KACC,OAAA6B,EAAA,GAAI,EAAGoC,aAAa,CAClBwT,QAAQ,EACRxT,aAIC,OAAAnB,EAAA,GAAG,CACR2U,QAAQ,EACRxT,OAAQ,KAId,OAAAX,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KDJvBkU,CAAYjX,EAAI,CAAEb,cAG5B+X,EAAQ,YAAa,QACxB3X,KACC,OAAA6B,EAAA,GAAI+V,GAAQ,YAAW,yBAA0BA,IACjD,OAAA/T,EAAA,GAAOgU,QAAoB,IAAPA,GACpB,OAAAC,EAAA,GAAQ,YAAa,iBACrB,OAAA5U,EAAA,GAAU,EAAE2U,EAAI9I,KAAW,YAAgB8I,EAAI,CAAEtP,UAASC,cACvDxI,KACC,OAAA6B,EAAA,GAAI,EAAGuG,QAAUjG,QACRA,GAAK0V,EAAG1T,aAAe,OAAS,QAEzC,OAAA2J,EAAA,KCIP,SACLrN,GAEA,OAAO,OAAAT,EAAA,GAGL,OAAA6M,EAAA,GAAUC,EAAA,GACV,OAAApJ,EAAA,GAAI5B,KCtFD,SACLrB,EAAiB9B,GAEjB8B,EAAG6J,aAAa,gBAAiB3L,EAAQ,SAAW,IDoFhDoZ,CAAqBtX,EAAa,SAATqB,KAI3B,OAAAsB,EAAA,GAAS,MChFN,SACL3C,GAEAA,EAAGuM,gBAAgB,iBD8EfgL,CAAuBvX,MDhBfwX,CAAgBlJ,KAGpB,OAAAhN,EAAA,GAAsB,SAI1B,OAAO,OAAAmG,EAAA,GAAc,CAACK,EAASoP,IAC5B3X,KACC,OAAA6B,EAAA,GAAI,EAAEuT,EAAQtT,KAAmB,OAAD,QAAGA,QAASsT,U,kLG/B/C,SAAS8C,GACd,QAAE3P,EAAO,UAAEC,IAEX,MAAMsL,EAAQ,IAAInR,EAAA,EAelB,OAZA,YAAa,UACV3C,KACC,OAAAkD,EAAA,GAAUkS,IAAUtB,SACjB9T,KACC,OAAA0I,EAAA,GAAwB,WCqChCjI,EDpC0B2U,ECsCnB,OAAApV,EAAA,GAGL,OAAA6M,EAAA,GAAUC,EAAA,GACV,OAAApJ,EAAA,GAAI,EAAG8L,cC3GJ,SACL/O,EAAiB9B,GAEjB8B,EAAG6J,aAAa,gBAAiB3L,EAAQ,SAAW,IDyGhDwZ,CAAgB1X,EAAI+O,KAItB,OAAApM,EAAA,GAAS,MCrGN,SACL3C,GAEAA,EAAGuM,gBAAgB,iBDmGfoL,CAAkB3X,QAbjB,IACLA,KDhCKP,UAAUgN,EAAA,GAGR,OAAAlN,EAAA,GACL,OAAAkD,EAAA,GAAUzC,GC7BP,SACLA,GAAiB,QAAE8H,EAAO,UAAEC,IAI5B,MAAM2M,EAAU5M,EACbvI,KACC,OAAA6B,EAAA,GAAI,EAAGoC,YAAaA,GACpB,OAAA6J,EAAA,MAIEuK,EAAUlD,EACbnV,KACC,OAAAkD,EAAA,GAAU,IAAM,YAAiBzC,GAC9BT,KACC,OAAA6B,EAAA,GAAI,EAAGoC,aAAa,CAClBqO,IAAQ7R,EAAGoI,UACXyP,OAAQ7X,EAAGoI,UAAY5E,KAEzB,OAAAyE,EAAA,GAAwB,aAMhC,OAAO,OAAAR,EAAA,GAAc,CAACiN,EAASkD,EAAS7P,IACrCxI,KACC,OAAA6B,EAAA,GAAI,EAAEuT,GAAU9C,MAAKgG,WAAYlQ,QAAUjG,KAAKkG,MAAQpE,eAK/C,CACLmE,OAAQkK,EAAM8C,EACdnR,OANFA,EAASuD,KAAKC,IAAI,EAAGxD,EACjBuD,KAAKC,IAAI,EAAG6K,EAASnQ,EAAIiT,GACzB5N,KAAKC,IAAI,EAAGxD,EAAS9B,EAAImW,IAK3B9I,OAAQ8C,EAAM8C,GAAUjT,KAG5B,OAAA2L,EAAA,GAA2B,CAACwG,EAAGC,IACtBD,EAAElM,SAAWmM,EAAEnM,QACfkM,EAAErQ,SAAWsQ,EAAEtQ,QACfqQ,EAAE9E,SAAW+E,EAAE/E,SDbV+I,CAAU9X,EAAI,CAAE8H,UAASC,eACzC,OAAA9E,EAAA,GAAIkU,GAAQ9D,EAAM7Q,KAAK2U,IACvB,OAAAxU,EAAA,GAAS,IAAM0Q,EAAM0E,e,kJGtClB,SAASC,GACd,QAAElQ,EAAO,UAAEC,EAAS,QAAEuL,IAEtB,OAAO,OAAA/T,EAAA,GACL,OAAAkD,EAAA,GAAUzC,GAAMsT,EACb/T,KACC,OAAAkD,EAAA,GAAU8Q,GAGJA,EACK,YAAgBvT,EAAI,CAAE8H,UAASC,cACnCxI,KACC,OAAA6B,EAAA,GAAI,EAAGuG,QAAUjG,SAAU,CAAGuW,OAAQvW,GAAK,MAC3C,OAAAuG,EAAA,GAAwB,UCpCjC,SACLjI,GAEA,OAAO,OAAAT,EAAA,GAGL,OAAA6M,EAAA,GAAUC,EAAA,GACV,OAAApJ,EAAA,GAAI,EAAGgV,cCrBJ,SACLjY,EAAiB9B,GAEjB8B,EAAG6J,aAAa,gBAAiB3L,EAAQ,SAAW,IDmBhDga,CAAclY,EAAIiY,KAIpB,OAAAtV,EAAA,GAAS,MCfN,SACL3C,GAEAA,EAAGuM,gBAAgB,iBDaf4L,CAAgBnY,MDwBNoY,CAAUpY,IAKP,OAAAqC,EAAA,GAAG,CAAE4V,QAAQ,U,6bGzChC,SAASI,IACP,MAAO,qBAAqBzT,KAAK0T,UAAUC,W,mBCe7C,SAASC,EACPlU,GAEA,MAAOjD,GAAQiD,EAAImU,MAAM,sBAAwB,GACjD,OAAQpX,EAAKqX,eAGX,IAAK,SACH,MAAO,CAAEC,EAAMC,GAAQtU,EAAImU,MAAM,yCACjC,OC9BC,SACLE,EAAcC,GAEd,MAAMtU,OAAsB,IAATsU,EACf,gCAAgCD,KAAQC,IACxC,gCAAgCD,EACpC,OAAO,OAAApY,EAAA,GAAKkN,MAAMnJ,GAAKqJ,KAAKC,GAAOA,EAAIiL,SACpCtZ,KACC,OAAA6B,EAAA,GAAIhG,IAGF,QAAoB,IAATwd,EAAsB,CAC/B,MAAM,iBAAEE,EAAgB,YAAEC,GAAsB3d,EAChD,MAAO,CACF,YAAM0d,GAAoB,GAA7B,SACG,YAAMC,GAAe,GAAxB,UAIG,CACL,MAAM,aAAEC,GAAuB5d,EAC/B,MAAO,CACF,YAAM4d,GAAgB,GAAzB,qBDQCC,CAA2BN,EAAMC,GAG1C,IAAK,SACH,MAAO,CAAExT,EAAM8T,GAAQ5U,EAAImU,MAAM,wCACjC,OEnCC,SACLrT,EAAc+T,GAEd,MAAM7U,EAAM,WAAWc,qBAAwBgU,mBAAmBD,KAClE,OAAO,OAAA5Y,EAAA,GAAKkN,MAAMnJ,GAAKqJ,KAAKC,GAAOA,EAAIiL,SACpCtZ,KACC,OAAA6B,EAAA,GAAI,EAAGiY,aAAYN,iBAAiC,CAC/C,YAAMM,GAAT,SACG,YAAMN,GAAT,YF2BKO,CAA2BlU,EAAM8T,GAG1C,QACE,OAAO,KG+BN,SAASK,EACdvZ,EAAiB9B,GAEjB8B,EAAG6J,aAAa,gBAAiB,QACjC7J,EAAG4R,MAAMC,IAAM,IAAI3T,MAQd,SAASsb,EACdxZ,GAEA,MAAM9B,GAAS,EAAIub,SAASzZ,EAAG4R,MAAMC,IAAK,IAC1C7R,EAAGuM,gBAAgB,iBACnBvM,EAAG4R,MAAMC,IAAM,GACX3T,GACFa,OAAOqI,SAAS,EAAGlJ,GAYhB,SAASwb,EAAW1Q,GACzB,IAAK,YAASA,GACZ,MAAM,IAAI2Q,YAAY,0BAA0BzP,KAAKI,UAAUtB,IAGjE,MAAM7J,EAAY,cACZkG,EAAY,cAGZ4N,EAAY,YAAkBjK,EAAO5D,KAAM,CAAEC,cAC7CuU,EAAY,cACZ7R,EAAY,cACZkM,EAAY,YAAW,sBACvBX,EAAY,YAAW,uBAK7B,0BAAgB,CACd,WACA,YACA,SACA,eACA,OACA,aACA,SACA,eACA,eACA,gBACA,OACA,OACA,OACC,CAAEnU,cAEL,MAAM2P,EAAY,cAGd5I,WAAW,WAAWI,SCjIrB,UACL,UAAEnH,EAAS,UAAE4I,IAEb,MAAM8R,EAAO1a,EACVI,KACC,OAAA6B,EAAA,GAAI,IAAM,YAA8B,gBAItC4G,EAAQD,EACXxI,KACC,OAAA0I,EAAA,GAAwB,SAI5B,OAAAR,EAAA,GAAc,CAACoS,EAAM7R,IAClBvI,UAAU,EAAEwP,MACX,IAAK,MAAMjP,KAAMiP,EACXjP,EAAG8Z,YAAc9Z,EAAG+Z,YACtB/Z,EAAG6J,aAAa,WAAY,KAE5B7J,EAAGuM,gBAAgB,cD6GzByN,CAAgB,CAAE7a,YAAW4I,cEzH1B,UACL,UAAE5I,EAAS,MAAEya,IAEb,MAAMC,EAAO1a,EACVI,KACC,OAAA6B,EAAA,GAAI,IAAM,YAAgC,aAI9C,OAAAD,EAAA,GACE,YAAW,SAAS5B,KAAK,OAAA6D,EAAA,GAAO0P,UAChC,OAAAzT,EAAA,GAAUN,OAAQ,gBAEjBQ,KACC,OAAAuJ,EAAA,GAAY+Q,IAEXpa,UAAUwP,IACT,IAAK,MAAMjP,KAAMiP,EACfjP,EAAG6J,aAAa,OAAQ,MAIhC+P,EACGra,KACC,OAAA6B,EAAA,GAAImK,GAAM,YAAW,QAAQA,QAC7B,OAAAnI,EAAA,GAAOpD,QAAoB,IAAPA,GACpB,OAAAiD,EAAA,GAAIjD,IACF,MAAMia,EAAUja,EAAGgN,QAAQ,WACvBiN,IAAYA,EAAQC,MACtBD,EAAQpQ,aAAa,OAAQ,OAGhCpK,UAAUO,GAAMA,EAAGma,kBF0FxBC,CAAa,CAAEjb,YAAWya,UGzHrB,UACL,UAAEza,IAEWA,EACVI,KACC,OAAAiO,EAAA,GAAK,GACL,OAAAa,EAAA,GAAe,uBAAa,cAC5B,OAAAjN,EAAA,GAAI,EAAE,CAAEpB,KAAQ,YAA+B,SAAUA,KAK1DT,KACC,OAAAkD,EAAA,GAAUwM,GAAO,OAAA5M,EAAA,MAAM4M,IACvB,OAAAoL,EAAA,GAAUra,IACR,MAAMsa,EAAS,YAAc,UAC7B,OAAIta,EAAGua,KACLD,EAAOC,IAAMva,EAAGua,IAChB,YAAeva,EAAIsa,GAGZ,IAAInU,EAAA,EAAWjD,IACpBoX,EAAOE,OAAS,IAAMtX,EAAS6U,eAKjCuC,EAAO5P,YAAc1K,EAAG0K,YACxB,YAAe1K,EAAIsa,GACZ,QAIV7a,UAAUgN,EAAA,GHyFfgO,CAAa,CAAEtb,cHtFV,UACL,UAAEA,IAEFA,EACGI,KACC,OAAA6B,EAAA,GAAI,IAAM,YAAqC,qBAC/C,OAAAqB,EAAA,GAAU,EAAG+B,UACX,WADsB,CAChB,GAAG,YAAKA,GAAS,IAAMgU,EAAiBhU,KAEhD,OAAAqJ,EAAA,GAAW,IAAM,MAEhBpO,UAAU8R,IACT,IAAK,MAAMvR,KAAM,YAAY,0BACtBA,EAAG0a,aAAa,mBACnB1a,EAAG6J,aAAa,gBAAiB,QACjC7J,EAAGkJ,YAAY,YAAaqI,OGwEtCoJ,CAAY,CAAExb,cIjIT,UACL,UAAEA,IAEF,MAAMyb,EAAW,YAAc,SAC/Bzb,EACGI,KACC,OAAA6B,EAAA,GAAI,IAAM,YAA8B,wBAEvC3B,UAAUwP,IACT,IAAK,MAAMjP,KAAMiP,EACf,YAAejP,EAAI4a,GACnB,YAAeA,EAAU,YAAY5a,MJuH7C6a,CAAY,CAAE1b,cJvHT,UACL,UAAEA,IAEF,MAAM0a,EAAO1a,EACVI,KACC,OAAA6B,EAAA,GAAI,IAAM,YAAY,wBACtB,OAAAyB,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAI3C8W,EAAKpa,UAAUwP,IACb,IAAK,MAAMjP,KAAMiP,EACfjP,EAAGuM,gBAAgB,uBAIvB,OAAAuO,EAAA,GAAIzC,EAAewB,EAAM,KACtBta,KACC,OAAAkD,EAAA,GAAUwM,GAAO,OAAA9N,EAAA,MAAS8N,EAAI7N,IAAIpB,GAChC,OAAAX,EAAA,GAAUW,EAAI,cACXT,KACC,OAAAC,EAAA,GAAMQ,QAIXP,UAAUO,IACT,MAAM6R,EAAM7R,EAAG2B,UAGH,IAARkQ,EACF7R,EAAG2B,UAAY,EAGNkQ,EAAM7R,EAAG0D,eAAiB1D,EAAGyW,eACtCzW,EAAG2B,UAAYkQ,EAAM,KIwF7BkJ,CAAe,CAAE5b,cAGjB,MAAM8L,EAAU,cACVQ,EAAa,YAAe,CAAEtM,YAAW8L,YAKzCnD,EAAU,uBAAa,UAC1BvI,KACC,sBAAY,CAAEJ,YAAW4I,cACzB,OAAAlF,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAGrCsQ,EAAQ,uBAAa,QACxB9T,KACC,oBAAU,CAAEuI,UAASC,cACrB,OAAAlF,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAKrCiY,EAAc,uBAAa,cAC9Bzb,KACC,0BAAgB,CAAEuI,UAASuL,QAAOtL,YAAWuL,YAC7C,OAAAzQ,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAGrCkY,EAAO,uBAAa,OACvB1b,KACC,+BAAqB,CAAEuI,UAASuL,QAAOtL,YAAWkM,YAClD,OAAApR,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAGrCmY,EAAQ,uBAAa,QACxB3b,KACC,oBAAU,CAAEuI,UAASC,YAAWuL,YAChC,OAAAzQ,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAkCrCoY,EA5BU,uBAAa,UAC1B5b,KACC,OAAAkD,EAAA,GAAU,IAAM,OAAAL,EAAA,GAAM,KACpB,MAAMgJ,EAAQpC,EAAOvC,QAAUuC,EAAOvC,OAAO2E,MACzCpC,EAAOvC,OAAO2E,WACdtL,EAGEkT,OACa,IAAV5H,EACH,OAAA7K,EAAA,GAAK6K,GACL6H,EACG1T,KACC,OAAAkD,EAAA,GAAU2C,GAAQqI,MAASrI,EAAH,4BAAoC,CAC1DsI,YAAa,gBACZC,KAAKC,GAAOA,EAAIiL,UAI7B,OAAO,OAAAxW,EAAA,GAAG,YAAkB2G,EAAOvC,OAAO6B,OAAQ,CAChD2K,QAAOD,gBASZzT,KACC,OAAAkD,EAAA,GAAU6F,IAER,MAAM4M,EAAS,uBAAa,gBACzB3V,KACC,2BAAiB+I,EAAQ,CAAEmN,UAAWzM,EAAOvC,OAAOgP,YACpD,OAAA5S,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAIrCoS,EAAS,uBAAa,gBACzB5V,KACC,6BACA,OAAAsD,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAIrCqS,EAAU,uBAAa,iBAC1B7V,KACC,4BAAkB+I,EAAQ,CAAE4M,WAC5B,OAAArS,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAG3C,OAAO,uBAAa,UACjBxD,KACC,sBAAY+I,EAAQ,CAAE4M,SAAQC,SAAQC,eAG5C,OAAAvH,EAAA,GAAW,KACT,uBAAa,UACVpO,UAAUO,GAAMA,EAAGiY,QAAS,GACxB,MAET,OAAApV,EAAA,GAAY,CAAEC,WAAY,EAAGC,UAAU,KAwD3C,GAlDA6W,EACGra,KACC,OAAA0D,EAAA,GAAI,IAAM,YAAU,UAAU,IAC9B,OAAAqJ,EAAA,GAAM,MAEL7M,UAAUsF,GAAQ,YAAgB,IAAIA,IAG3C,OAAA0C,EAAA,GAAc,CACZ,YAAY,UACZwM,IAEC1U,KACC,OAAA8O,EAAA,GAAetG,GACf,OAAAtF,EAAA,GAAU,GAAGmN,EAAQsE,IAAWvM,QAAUjG,UACxC,MAAMqN,EAASa,IAAWsE,EAC1B,OAAO/U,EACJI,KACC,OAAA+M,EAAA,GAAMyC,EAAS,IAAM,KACrB,OAAA3C,EAAA,GAAUC,EAAA,GACV,OAAApJ,EAAA,GAAI,EAAGiJ,UAAW6C,EACdwK,EAAcrN,EAAMxK,GACpB8X,EAAgBtN,QAKzBzM,YAKL,OAAAJ,EAAA,GAAsBC,SAAS4M,KAAM,SAClC3M,KACC,OAAA6D,EAAA,GAAOW,KAAQA,EAAGC,SAAWD,EAAGE,UAChC,OAAAb,EAAA,GAAOW,IACL,GAAIA,EAAGlD,kBAAkBT,YAAa,CACpC,MAAMJ,EAAK+D,EAAGlD,OAAOmM,QAAQ,KAC7B,GAAIhN,GAAM,YAAgBA,GACxB,OAAO,EAGX,OAAO,KAGRP,UAAU,KACT,YAAU,UAAU,KAKxBuJ,EAAOC,SAASgE,SAAS,uBACH,UAAtB1I,SAAS6W,SACT,CACA,MAAMpN,EAAM,IAAIC,UAGhBgF,EACG1T,KACC,OAAAkD,EAAA,GAAU2C,GAAQ,OAAA7E,EAAA,GAAKkN,MAASrI,EAAH,gBAC1BuI,KAAKC,GAAOA,EAAI3B,QAChB0B,KAAK1B,GAAQ+B,EAAIG,gBAAgBlC,EAAM,eAE1C,OAAAoC,EAAA,GAAe4E,GACf,OAAA7R,EAAA,GAAI,EAAE9B,EAAU8F,MACd,MAAMuH,EAAO,YAAY,MAAOrN,GAC7B8B,IAAIxB,GAAQA,EAAK8K,aAQpB,GAAIiC,EAAK/Q,OAAS,EAAG,CACnB,MAAOiY,EAAGC,GAAKnH,EAAK0O,KAAK,CAACxH,EAAGC,IAAMD,EAAEjY,OAASkY,EAAElY,QAGhD,IAAIwP,EAAQ,EACZ,GAAIyI,IAAMC,EACR1I,EAAQyI,EAAEjY,YAEV,KAAOiY,EAAEyH,OAAOlQ,KAAW0I,EAAEwH,OAAOlQ,IAClCA,IAGJ,IAAK,IAAI1P,EAAI,EAAGA,EAAIiR,EAAK/Q,OAAQF,IAC/BiR,EAAKjR,GAAKiR,EAAKjR,GAAG8J,QAAQqO,EAAE5U,MAAM,EAAGmM,GAAWhG,EAAH,KAEjD,OAAOuH,KAGRlN,UAAUkN,IACT,YAAoBA,EAAM,CAAExN,YAAWkG,YAAW0C,gBAO1D+G,EACGvP,KACC,OAAA6D,EAAA,GAAO5E,GAAoB,WAAbA,EAAIJ,MAAkC,QAAbI,EAAI6C,MAC3C,OAAAiE,EAAA,GAAK,IAEJ7F,UAAU,KACT,IAAK,MAAM8b,KAAQ,YAAY,eAC7BA,EAAK3J,MAAM4J,WAAa,YAKhC,MAAMpO,EAAQ,CAGZjO,YACAkG,YACA0C,YAGAD,UACAuL,QACA2H,cACAG,UACAD,QACAD,OAGAxP,aACAqD,YACA7D,WAMF,OAFA,OAAA9J,EAAA,MAAStF,OAAO4f,OAAOrO,IACpB3N,YACI2N,EAjVT9N,SAASoc,gBAAgB3P,UAAUS,OAAO,SAC1ClN,SAASoc,gBAAgB3P,UAAUC,IAAI,MAGnCsM,UAAUC,UAAUE,MAAM,wBAC5BnZ,SAASoc,gBAAgB3P,UAAUC,IAAI","file":"assets/javascripts/bundle.9554a270.min.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t0: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \tvar jsonpArray = window[\"webpackJsonp\"] = window[\"webpackJsonp\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// add entry module to deferred list\n \tdeferredModules.push([74,1]);\n \t// run deferred modules when ready\n \treturn checkDeferredModules();\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { ReplaySubject, Subject, fromEvent } from \"rxjs\"\nimport { mapTo } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch document\n *\n * Documents must be implemented as subjects, so all downstream observables are\n * automatically updated when a new document is emitted. This enabled features\n * like instant loading.\n *\n * @return Document subject\n */\nexport function watchDocument(): Subject {\n const document$ = new ReplaySubject()\n fromEvent(document, \"DOMContentLoaded\")\n .pipe(\n mapTo(document)\n )\n .subscribe(document$)\n\n /* Return document */\n return document$\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve an element matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @return Element or nothing\n */\nexport function getElement(\n selector: string, node: ParentNode = document\n): T | undefined {\n return node.querySelector(selector) || undefined\n}\n\n/**\n * Retrieve an element matching a query selector or throw a reference error\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @return Element\n */\nexport function getElementOrThrow(\n selector: string, node: ParentNode = document\n): T {\n const el = getElement(selector, node)\n if (typeof el === \"undefined\")\n throw new ReferenceError(\n `Missing element: expected \"${selector}\" to be present`\n )\n return el\n}\n\n/**\n * Retrieve the currently active element\n *\n * @return Element or nothing\n */\nexport function getActiveElement(): HTMLElement | undefined {\n return document.activeElement instanceof HTMLElement\n ? document.activeElement\n : undefined\n}\n\n/**\n * Retrieve all elements matching the query selector\n *\n * @template T - Element type\n *\n * @param selector - Query selector\n * @param node - Node of reference\n *\n * @return Elements\n */\nexport function getElements(\n selector: string, node: ParentNode = document\n): T[] {\n return Array.from(node.querySelectorAll(selector))\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Create an element\n *\n * @template T - Tag name type\n *\n * @param tagName - Tag name\n *\n * @return Element\n */\nexport function createElement(\n tagName: T\n): HTMLElementTagNameMap[T] {\n return document.createElement(tagName)\n}\n\n/**\n * Replace an element with another element\n *\n * @param source - Source element\n * @param target - Target element\n */\nexport function replaceElement(\n source: HTMLElement, target: Node\n): void {\n source.replaceWith(target)\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent, merge } from \"rxjs\"\nimport { map, startWith } from \"rxjs/operators\"\n\nimport { getActiveElement } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set element focus\n *\n * @param el - Element\n * @param value - Whether the element should be focused\n */\nexport function setElementFocus(\n el: HTMLElement, value: boolean = true\n): void {\n if (value)\n el.focus()\n else\n el.blur()\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch element focus\n *\n * @param el - Element\n *\n * @return Element focus observable\n */\nexport function watchElementFocus(\n el: HTMLElement\n): Observable {\n return merge(\n fromEvent(el, \"focus\"),\n fromEvent(el, \"blur\")\n )\n .pipe(\n map(({ type }) => type === \"focus\"),\n startWith(el === getActiveElement())\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent, merge } from \"rxjs\"\nimport { map, startWith } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Element offset\n */\nexport interface ElementOffset {\n x: number /* Horizontal offset */\n y: number /* Vertical offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve element offset\n *\n * @param el - Element\n *\n * @return Element offset\n */\nexport function getElementOffset(el: HTMLElement): ElementOffset {\n return {\n x: el.scrollLeft,\n y: el.scrollTop\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch element offset\n *\n * @param el - Element\n *\n * @return Element offset observable\n */\nexport function watchElementOffset(\n el: HTMLElement\n): Observable {\n return merge(\n fromEvent(el, \"scroll\"),\n fromEvent(window, \"resize\")\n )\n .pipe(\n map(() => getElementOffset(el)),\n startWith(getElementOffset(el))\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set element text selection\n *\n * @param el - Element\n */\nexport function setElementSelection(\n el: HTMLElement\n): void {\n if (el instanceof HTMLInputElement)\n el.select()\n else\n throw new Error(\"Not implemented\")\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport ResizeObserver from \"resize-observer-polyfill\"\nimport {\n NEVER,\n Observable,\n Subject,\n defer,\n merge,\n of\n} from \"rxjs\"\nimport {\n filter,\n finalize,\n map,\n shareReplay,\n startWith,\n switchMap,\n tap\n} from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Element offset\n */\nexport interface ElementSize {\n width: number /* Element width */\n height: number /* Element height */\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Resize observer entry subject\n */\nconst entry$ = new Subject()\n\n/**\n * Resize observer observable\n *\n * This observable will create a `ResizeObserver` on the first subscription\n * and will automatically terminate it when there are no more subscribers.\n * It's quite important to centralize observation in a single `ResizeObserver`,\n * as the performance difference can be quite dramatic, as the link shows.\n *\n * @see https://bit.ly/3iIYfEm - Google Groups on performance\n */\nconst observer$ = defer(() => of(\n new ResizeObserver(entries => {\n for (const entry of entries)\n entry$.next(entry)\n })\n))\n .pipe(\n switchMap(resize => merge(of(resize), NEVER)\n .pipe(\n finalize(() => resize.disconnect())\n )\n ),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve element size\n *\n * @param el - Element\n *\n * @return Element size\n */\nexport function getElementSize(el: HTMLElement): ElementSize {\n return {\n width: el.offsetWidth,\n height: el.offsetHeight\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch element size\n *\n * This function returns an observable that will subscribe to a single internal\n * instance of `ResizeObserver` upon subscription, and emit resize events until\n * termination. Note that this function should not be called with the same\n * element twice, as the first unsubscription will terminate observation.\n *\n * @param el - Element\n *\n * @return Element size observable\n */\nexport function watchElementSize(\n el: HTMLElement\n): Observable {\n return observer$\n .pipe(\n tap(observer => observer.observe(el)),\n switchMap(observer => entry$\n .pipe(\n filter(({ target }) => target === el),\n finalize(() => observer.unobserve(el)),\n map(({ contentRect }) => ({\n width: contentRect.width,\n height: contentRect.height\n }))\n )\n ),\n startWith(getElementSize(el))\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent } from \"rxjs\"\nimport { filter, map, share } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Key\n */\nexport interface Key {\n type: string /* Key type */\n claim(): void /* Key claim */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Check whether an element may receive keyboard input\n *\n * @param el - Element\n *\n * @return Test result\n */\nexport function isSusceptibleToKeyboard(el: HTMLElement): boolean {\n switch (el.tagName) {\n\n /* Form elements */\n case \"INPUT\":\n case \"SELECT\":\n case \"TEXTAREA\":\n return true\n\n /* Everything else */\n default:\n return el.isContentEditable\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch keyboard\n *\n * @return Keyboard observable\n */\nexport function watchKeyboard(): Observable {\n return fromEvent(window, \"keydown\")\n .pipe(\n filter(ev => !(ev.metaKey || ev.ctrlKey)),\n map(ev => ({\n type: ev.key,\n claim() {\n ev.preventDefault()\n ev.stopPropagation()\n }\n })),\n share()\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { BehaviorSubject, Subject } from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve location\n *\n * This function will return a `URL` object (and not `Location`) in order to\n * normalize typings across the application. Furthermore, locations need to be\n * tracked without setting them and `Location` is a singleton which represents\n * the current location.\n *\n * @return URL\n */\nexport function getLocation(): URL {\n return new URL(location.href)\n}\n\n/**\n * Set location\n *\n * @param url - URL to change to\n */\nexport function setLocation(url: URL): void {\n location.href = url.href\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Check whether a URL is a local link or a file (except `.html`)\n *\n * @param url - URL or HTML anchor element\n * @param ref - Reference URL\n *\n * @return Test result\n */\nexport function isLocalLocation(\n url: URL | HTMLAnchorElement,\n ref: URL | Location = location\n): boolean {\n return url.host === ref.host\n && /^(?:\\/[\\w-]+)*(?:\\/?|\\.html)$/i.test(url.pathname)\n}\n\n/**\n * Check whether a URL is an anchor link on the current page\n *\n * @param url - URL or HTML anchor element\n * @param ref - Reference URL\n *\n * @return Test result\n */\nexport function isAnchorLocation(\n url: URL | HTMLAnchorElement,\n ref: URL | Location = location\n): boolean {\n return url.pathname === ref.pathname\n && url.hash.length > 0\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch location\n *\n * @return Location subject\n */\nexport function watchLocation(): Subject {\n return new BehaviorSubject(getLocation())\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable } from \"rxjs\"\nimport { map, shareReplay, take } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n location$: Observable /* Location observable */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch location base\n *\n * @return Location base observable\n */\nexport function watchLocationBase(\n base: string, { location$ }: WatchOptions\n): Observable {\n return location$\n .pipe(\n take(1),\n map(({ href }) => new URL(base, href)\n .toString()\n .replace(/\\/$/, \"\")\n ),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent } from \"rxjs\"\nimport { filter, map, share, startWith } from \"rxjs/operators\"\n\nimport { createElement } from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve location hash\n *\n * @return Location hash\n */\nexport function getLocationHash(): string {\n return location.hash.substring(1)\n}\n\n/**\n * Set location hash\n *\n * Setting a new fragment identifier via `location.hash` will have no effect\n * if the value doesn't change. When a new fragment identifier is set, we want\n * the browser to target the respective element at all times, which is why we\n * use this dirty little trick.\n *\n * @param hash - Location hash\n */\nexport function setLocationHash(hash: string): void {\n const el = createElement(\"a\")\n el.href = hash\n el.addEventListener(\"click\", ev => ev.stopPropagation())\n el.click()\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch location hash\n *\n * @return Location hash observable\n */\nexport function watchLocationHash(): Observable {\n return fromEvent(window, \"hashchange\")\n .pipe(\n map(getLocationHash),\n startWith(getLocationHash()),\n filter(hash => hash.length > 0),\n share()\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable } from \"rxjs\"\nimport { shareReplay, startWith } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch media query\n *\n * @param query - Media query\n *\n * @return Media observable\n */\nexport function watchMedia(query: string): Observable {\n const media = matchMedia(query)\n return new Observable(subscriber => {\n media.addListener(ev => subscriber.next(ev.matches))\n })\n .pipe(\n startWith(media.matches),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent } from \"rxjs\"\nimport { map, startWith } from \"rxjs/operators\"\n\nimport { getElementOrThrow } from \"../element\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Toggle\n */\nexport type Toggle =\n | \"drawer\" /* Toggle for drawer */\n | \"search\" /* Toggle for search */\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Toggle map\n */\nconst toggles: Record = {\n drawer: getElementOrThrow(`[data-md-toggle=drawer]`),\n search: getElementOrThrow(`[data-md-toggle=search]`)\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve the value of a toggle\n *\n * @param name - Toggle\n *\n * @return Toggle value\n */\nexport function getToggle(name: Toggle): boolean {\n return toggles[name].checked\n}\n\n/**\n * Set toggle\n *\n * Simulating a click event seems to be the most cross-browser compatible way\n * of changing the value while also emitting a `change` event. Before, Material\n * used `CustomEvent` to programmatically change the value of a toggle, but this\n * is a much simpler and cleaner solution which doesn't require a polyfill.\n *\n * @param name - Toggle\n * @param value - Toggle value\n */\nexport function setToggle(name: Toggle, value: boolean): void {\n if (toggles[name].checked !== value)\n toggles[name].click()\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch toggle\n *\n * @param name - Toggle\n *\n * @return Toggle value observable\n */\nexport function watchToggle(name: Toggle): Observable {\n const el = toggles[name]\n return fromEvent(el, \"change\")\n .pipe(\n map(() => el.checked),\n startWith(el.checked)\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent, merge } from \"rxjs\"\nimport { map, startWith } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Viewport offset\n */\nexport interface ViewportOffset {\n x: number /* Horizontal offset */\n y: number /* Vertical offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve viewport offset\n *\n * On iOS Safari, viewport offset can be negative due to overflow scrolling.\n * As this may induce strange behaviors downstream, we'll just limit it to 0.\n *\n * @return Viewport offset\n */\nexport function getViewportOffset(): ViewportOffset {\n return {\n x: Math.max(0, pageXOffset),\n y: Math.max(0, pageYOffset)\n }\n}\n\n/**\n * Set viewport offset\n *\n * @param offset - Viewport offset\n */\nexport function setViewportOffset(\n { x, y }: Partial\n): void {\n window.scrollTo(x || 0, y || 0)\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport offset\n *\n * @return Viewport offset observable\n */\nexport function watchViewportOffset(): Observable {\n return merge(\n fromEvent(window, \"scroll\", { passive: true }),\n fromEvent(window, \"resize\", { passive: true })\n )\n .pipe(\n map(getViewportOffset),\n startWith(getViewportOffset())\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent } from \"rxjs\"\nimport { map, startWith } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Viewport size\n */\nexport interface ViewportSize {\n width: number /* Viewport width */\n height: number /* Viewport height */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Retrieve viewport size\n *\n * @return Viewport size\n */\nexport function getViewportSize(): ViewportSize {\n return {\n width: innerWidth,\n height: innerHeight\n }\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport size\n *\n * @return Viewport size observable\n */\nexport function watchViewportSize(): Observable {\n return fromEvent(window, \"resize\", { passive: true })\n .pipe(\n map(getViewportSize),\n startWith(getViewportSize())\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, combineLatest } from \"rxjs\"\nimport {\n distinctUntilKeyChanged,\n map,\n shareReplay\n} from \"rxjs/operators\"\n\nimport { Header } from \"components\"\n\nimport {\n ViewportOffset,\n watchViewportOffset\n} from \"../offset\"\nimport {\n ViewportSize,\n watchViewportSize\n} from \"../size\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Viewport\n */\nexport interface Viewport {\n offset: ViewportOffset /* Viewport offset */\n size: ViewportSize /* Viewport size */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch at options\n */\ninterface WatchAtOptions {\n header$: Observable
/* Header observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch viewport\n *\n * @return Viewport observable\n */\nexport function watchViewport(): Observable {\n return combineLatest([\n watchViewportOffset(),\n watchViewportSize()\n ])\n .pipe(\n map(([offset, size]) => ({ offset, size })),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n}\n\n/**\n * Watch viewport relative to element\n *\n * @param el - Element\n * @param options - Options\n *\n * @return Viewport observable\n */\nexport function watchViewportAt(\n el: HTMLElement, { header$, viewport$ }: WatchAtOptions\n): Observable {\n const size$ = viewport$\n .pipe(\n distinctUntilKeyChanged(\"size\")\n )\n\n /* Compute element offset */\n const offset$ = combineLatest([size$, header$])\n .pipe(\n map((): ViewportOffset => ({\n x: el.offsetLeft,\n y: el.offsetTop\n }))\n )\n\n /* Compute relative viewport, return hot observable */\n return combineLatest([header$, viewport$, offset$])\n .pipe(\n map(([{ height }, { offset, size }, { x, y }]) => ({\n offset: {\n x: offset.x - x,\n y: offset.y - y + height\n },\n size\n }))\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, Subject, fromEvent } from \"rxjs\"\nimport {\n map,\n share,\n switchMapTo,\n tap,\n throttle\n} from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Worker message\n */\nexport interface WorkerMessage {\n type: unknown /* Message type */\n data?: unknown /* Message data */\n}\n\n/**\n * Worker handler\n *\n * @template T - Message type\n */\nexport interface WorkerHandler<\n T extends WorkerMessage\n> {\n tx$: Subject /* Message transmission subject */\n rx$: Observable /* Message receive observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n *\n * @template T - Worker message type\n */\ninterface WatchOptions {\n tx$: Observable /* Message transmission observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch a web worker\n *\n * This function returns an observable that will send all values emitted by the\n * message observable to the web worker. Web worker communication is expected\n * to be bidirectional (request-response) and synchronous. Messages that are\n * emitted during a pending request are throttled, the last one is emitted.\n *\n * @param worker - Web worker\n * @param options - Options\n *\n * @return Worker message observable\n */\nexport function watchWorker(\n worker: Worker, { tx$ }: WatchOptions\n): Observable {\n\n /* Intercept messages from worker-like objects */\n const rx$ = fromEvent(worker, \"message\")\n .pipe(\n map(({ data }) => data)\n )\n\n /* Send and receive messages, return hot observable */\n return tx$\n .pipe(\n throttle(() => rx$, { leading: true, trailing: true }),\n tap(message => worker.postMessage(message)),\n switchMapTo(rx$),\n share()\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SearchIndex, SearchTransformFn } from \"integrations\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Feature flags\n */\nexport type Feature =\n | \"navigation.tabs\" /* Tabs navigation */\n | \"navigation.instant\" /* Instant loading */\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Configuration\n */\nexport interface Config {\n base: string /* Base URL */\n features: Feature[] /* Feature flags */\n search: {\n worker: string /* Worker URL */\n index?: Promise /* Promise resolving with index */\n transform?: SearchTransformFn /* Transformation function */\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Ensure that the given value is a valid configuration\n *\n * We could use `jsonschema` or any other schema validation framework, but that\n * would just add more bloat to the bundle, so we'll keep it plain and simple.\n *\n * @param config - Configuration\n *\n * @return Test result\n */\nexport function isConfig(config: any): config is Config {\n return typeof config === \"object\"\n && typeof config.base === \"string\"\n && typeof config.features === \"object\"\n && typeof config.search === \"object\"\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n// tslint:disable no-null-keyword\n\nimport { JSX as JSXInternal } from \"preact\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * HTML attributes\n */\ntype Attributes =\n & JSXInternal.HTMLAttributes\n & JSXInternal.SVGAttributes\n & Record\n\n/**\n * Child element\n */\ntype Child =\n | HTMLElement\n | Text\n | string\n | number\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Append a child node to an element\n *\n * @param el - Element\n * @param child - Child node(s)\n */\nfunction appendChild(el: HTMLElement, child: Child | Child[]): void {\n\n /* Handle primitive types (including raw HTML) */\n if (typeof child === \"string\" || typeof child === \"number\") {\n el.innerHTML += child.toString()\n\n /* Handle nodes */\n } else if (child instanceof Node) {\n el.appendChild(child)\n\n /* Handle nested children */\n } else if (Array.isArray(child)) {\n for (const node of child)\n appendChild(el, node)\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * JSX factory\n *\n * @param tag - HTML tag\n * @param attributes - HTML attributes\n * @param children - Child elements\n *\n * @return Element\n */\nexport function h(\n tag: string, attributes: Attributes | null, ...children: Child[]\n): HTMLElement {\n const el = document.createElement(tag)\n\n /* Set attributes, if any */\n if (attributes)\n for (const attr of Object.keys(attributes))\n if (typeof attributes[attr] !== \"boolean\")\n el.setAttribute(attr, attributes[attr])\n else if (attributes[attr])\n el.setAttribute(attr, \"\")\n\n /* Append child nodes */\n for (const child of children)\n appendChild(el, child)\n\n /* Return element */\n return el\n}\n\n/* ----------------------------------------------------------------------------\n * Namespace\n * ------------------------------------------------------------------------- */\n\nexport declare namespace h {\n namespace JSX {\n type Element = HTMLElement\n type IntrinsicElements = JSXInternal.IntrinsicElements\n }\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, defer, of } from \"rxjs\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Cache the last value emitted by an observable in session storage\n *\n * If the key is not found in session storage, the factory is executed and the\n * latest value emitted will automatically be persisted to sessions storage.\n * Note that the values emitted by the returned observable must be serializable\n * as `JSON`, or data will be lost.\n *\n * @template T - Value type\n *\n * @param key - Cache key\n * @param factory - Observable factory\n *\n * @return Value observable\n */\nexport function cache(\n key: string, factory: () => Observable\n): Observable {\n return defer(() => {\n const data = sessionStorage.getItem(key)\n if (data) {\n return of(JSON.parse(data) as T)\n\n /* Retrieve value from observable factory and write to storage */\n } else {\n const value$ = factory()\n value$.subscribe(value => {\n try {\n sessionStorage.setItem(key, JSON.stringify(value))\n } catch (err) {\n /* Uncritical, just swallow */\n }\n })\n\n /* Return value */\n return value$\n }\n })\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { getElementOrThrow } from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Translation keys\n */\ntype TranslateKey =\n | \"clipboard.copy\" /* Copy to clipboard */\n | \"clipboard.copied\" /* Copied to clipboard */\n | \"search.config.lang\" /* Search language */\n | \"search.config.pipeline\" /* Search pipeline */\n | \"search.config.separator\" /* Search separator */\n | \"search.result.placeholder\" /* Type to start searching */\n | \"search.result.none\" /* No matching documents */\n | \"search.result.one\" /* 1 matching document */\n | \"search.result.other\" /* # matching documents */\n | \"search.result.more.one\" /* 1 more on this page */\n | \"search.result.more.other\" /* # more on this page */\n | \"search.result.term.missing\" /* Missing */\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Translations\n */\nlet lang: Record\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Translate the given key\n *\n * @param key - Key to be translated\n * @param value - Value to be replaced\n *\n * @return Translation\n */\nexport function translate(\n key: TranslateKey, value?: string | number\n): string {\n if (typeof lang === \"undefined\") {\n const el = getElementOrThrow(\"#__lang\")\n lang = JSON.parse(el.textContent!)\n }\n if (typeof lang[key] === \"undefined\") {\n throw new ReferenceError(`Invalid translation: ${key}`)\n }\n return typeof value !== \"undefined\"\n ? lang[key].replace(\"#\", value.toString())\n : lang[key]\n}\n\n/**\n * Truncate a string after the given number of characters\n *\n * This is not a very reasonable approach, since the summaries kind of suck.\n * It would be better to create something more intelligent, highlighting the\n * search occurrences and making a better summary out of it, but this note was\n * written three years ago, so who knows if we'll ever fix it.\n *\n * @param value - Value to be truncated\n * @param n - Number of characters\n *\n * @return Truncated value\n */\nexport function truncate(value: string, n: number): string {\n let i = n\n if (value.length > i) {\n while (value[i] !== \" \" && --i > 0); // tslint:disable-line\n return `${value.substring(0, i)}...`\n }\n return value\n}\n\n/**\n * Round a number for display with source facts\n *\n * This is a reverse engineered version of GitHub's weird rounding algorithm\n * for stars, forks and all other numbers. While all numbers below `1,000` are\n * returned as-is, bigger numbers are converted to fixed numbers:\n *\n * - `1,049` => `1k`\n * - `1,050` => `1.1k`\n * - `1,949` => `1.9k`\n * - `1,950` => `2k`\n *\n * @param value - Original value\n *\n * @return Rounded value\n */\nexport function round(value: number): string {\n if (value > 999) {\n const digits = +((value - 950) % 1000 > 99)\n return `${((value + 0.000001) / 1000).toFixed(digits)}k`\n } else {\n return value.toString()\n }\n}\n\n/**\n * Simple hash function\n *\n * @see https://bit.ly/2wsVjJ4 - Original source\n *\n * @param value - Value to be hashed\n *\n * @return Hash as 32bit integer\n */\nexport function hash(value: string): number {\n let h = 0\n for (let i = 0, len = value.length; i < len; i++) {\n h = ((h << 5) - h) + value.charCodeAt(i)\n h |= 0 // Convert to 32bit integer\n }\n return h\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nexport * from \"./_\"\nexport * from \"./header\"\nexport * from \"./main\"\nexport * from \"./navigation\"\nexport * from \"./search\"\nexport * from \"./shared\"\nexport * from \"./tabs\"\nexport * from \"./toc\"\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport * as ClipboardJS from \"clipboard\"\nimport { NEVER, Observable, Subject } from \"rxjs\"\nimport { mapTo, share, tap } from \"rxjs/operators\"\n\nimport { getElements } from \"browser\"\nimport { renderClipboardButton } from \"templates\"\nimport { translate } from \"utilities\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n document$: Observable /* Document observable */\n dialog$: Subject /* Dialog subject */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up clipboard\n *\n * This function implements the Clipboard.js integration and injects a button\n * into all code blocks when the document changes.\n *\n * @param options - Options\n *\n * @return Clipboard observable\n */\nexport function setupClipboard(\n { document$, dialog$ }: SetupOptions\n): Observable {\n if (!ClipboardJS.isSupported())\n return NEVER\n\n /* Inject 'copy-to-clipboard' buttons */\n document$.subscribe(() => {\n const blocks = getElements(\"pre > code\")\n blocks.forEach((block, index) => {\n const parent = block.parentElement!\n parent.id = `__code_${index}`\n parent.insertBefore(\n renderClipboardButton(parent.id),\n block\n )\n })\n })\n\n /* Initialize clipboard */\n const clipboard$ = new Observable(subscriber => {\n new ClipboardJS(\".md-clipboard\").on(\"success\", ev => subscriber.next(ev))\n })\n .pipe(\n share()\n )\n\n /* Display notification for clipboard event */\n clipboard$\n .pipe(\n tap(ev => ev.clearSelection()),\n mapTo(translate(\"clipboard.copied\"))\n )\n .subscribe(dialog$)\n\n /* Return clipboard */\n return clipboard$\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Subject, animationFrameScheduler, noop, of } from \"rxjs\"\nimport {\n delay,\n map,\n observeOn,\n switchMap,\n tap\n} from \"rxjs/operators\"\n\nimport { createElement } from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n duration?: number /* Display duration (default: 2s) */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up dialog\n *\n * @param options - Options\n *\n * @return Dialog observable\n */\nexport function setupDialog(\n { duration }: SetupOptions = {}\n): Subject {\n const dialog$ = new Subject()\n\n /* Create dialog */\n const dialog = createElement(\"div\") // TODO: improve scoping\n dialog.classList.add(\"md-dialog\", \"md-typeset\")\n\n /* Display dialog */\n dialog$\n .pipe(\n switchMap(text => of(document.body) // useComponent(\"container\")\n .pipe(\n map(container => container.appendChild(dialog)),\n observeOn(animationFrameScheduler),\n delay(1), // Strangley it doesnt work when we push things to the new animation frame...\n tap(el => {\n el.innerHTML = text\n el.setAttribute(\"data-md-state\", \"open\")\n }),\n delay(duration || 2000),\n tap(el => el.removeAttribute(\"data-md-state\")),\n delay(400),\n tap(el => {\n el.innerHTML = \"\"\n el.remove()\n })\n )\n )\n )\n .subscribe(noop)\n\n /* Return dialog */\n return dialog$\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { NEVER, Observable, Subject, from, fromEvent, merge, of } from \"rxjs\"\nimport {\n bufferCount,\n catchError,\n debounceTime,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n filter,\n map,\n sample,\n share,\n skip,\n switchMap,\n withLatestFrom\n} from \"rxjs/operators\"\n\nimport {\n Viewport,\n ViewportOffset,\n getElement,\n isAnchorLocation,\n isLocalLocation,\n replaceElement,\n setLocation,\n setLocationHash,\n setToggle,\n setViewportOffset\n} from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * History state\n */\ninterface State {\n url: URL /* State URL */\n offset?: ViewportOffset /* State viewport offset */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n document$: Subject /* Document subject */\n location$: Subject /* Location subject */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up instant loading\n *\n * When fetching, theoretically, we could use `responseType: \"document\"`, but\n * since all MkDocs links are relative, we need to make sure that the current\n * location matches the document we just loaded. Otherwise any relative links\n * in the document could use the old location.\n *\n * This is the reason why we need to synchronize history events and the process\n * of fetching the document for navigation changes (except `popstate` events):\n *\n * 1. Fetch document via `XMLHTTPRequest`\n * 2. Set new location via `history.pushState`\n * 3. Parse and emit fetched document\n *\n * For `popstate` events, we must not use `history.pushState`, or the forward\n * history will be irreversibly overwritten. In case the request fails, the\n * location change is dispatched regularly.\n *\n * @param options - Options\n */\nexport function setupInstantLoading(\n urls: string[], { document$, viewport$, location$ }: SetupOptions\n): void {\n\n /* Disable automatic scroll restoration */\n if (\"scrollRestoration\" in history)\n history.scrollRestoration = \"manual\"\n\n /* Hack: ensure that reloads restore viewport offset */\n fromEvent(window, \"beforeunload\")\n .subscribe(() => {\n history.scrollRestoration = \"auto\"\n })\n\n /* Hack: ensure absolute favicon link to omit 404s on document switch */\n const favicon = getElement(`link[rel=\"shortcut icon\"]`)\n if (typeof favicon !== \"undefined\")\n favicon.href = favicon.href // tslint:disable-line no-self-assignment\n\n /* Intercept link clicks and convert to state change */\n const state$ = fromEvent(document.body, \"click\")\n .pipe(\n filter(ev => !(ev.metaKey || ev.ctrlKey)),\n switchMap(ev => {\n if (ev.target instanceof HTMLElement) {\n const el = ev.target.closest(\"a\")\n if (\n el && !el.target &&\n isLocalLocation(el) &&\n urls.includes(el.href)\n ) {\n if (!isAnchorLocation(el))\n ev.preventDefault()\n return of(el)\n }\n }\n return NEVER\n }),\n map(el => ({ url: new URL(el.href) })),\n share()\n )\n\n /* Always close search on link click */\n state$.subscribe(() => {\n setToggle(\"search\", false)\n })\n\n /* Filter state changes to dispatch */\n const push$ = state$\n .pipe(\n filter(({ url }) => !isAnchorLocation(url)),\n share()\n )\n\n /* Intercept popstate events (history back and forward) */\n const pop$ = fromEvent(window, \"popstate\")\n .pipe(\n filter(ev => ev.state !== null),\n map(ev => ({\n url: new URL(location.href),\n offset: ev.state\n })),\n share()\n )\n\n /* Emit location change */\n merge(push$, pop$)\n .pipe(\n distinctUntilChanged((prev, next) => prev.url.href === next.url.href),\n map(({ url }) => url)\n )\n .subscribe(location$)\n\n /* Fetch document on location change */\n const ajax$ = location$\n .pipe(\n distinctUntilKeyChanged(\"pathname\"),\n skip(1),\n switchMap(url => from(fetch(url.href, {\n credentials: \"same-origin\"\n }).then(res => res.text()))\n .pipe(\n catchError(() => {\n setLocation(url)\n return NEVER\n })\n )\n ),\n share()\n )\n\n /* Set new location as soon as the document was fetched */\n push$\n .pipe(\n sample(ajax$)\n )\n .subscribe(({ url }) => {\n history.pushState({}, \"\", url.toString())\n })\n\n /* Parse and emit document */\n const dom = new DOMParser()\n ajax$\n .pipe(\n map(response => dom.parseFromString(response, \"text/html\"))\n )\n .subscribe(document$)\n\n /* Intercept instant loading */\n const instant$ = merge(push$, pop$)\n .pipe(\n sample(document$)\n )\n\n // TODO: this must be combined with search scroll restoration on mobile\n instant$.subscribe(({ url, offset }) => {\n if (url.hash && !offset) {\n setLocationHash(url.hash)\n } else {\n setViewportOffset(offset || { y: 0 })\n }\n })\n\n /* Replace document metadata */\n instant$\n .pipe(\n withLatestFrom(document$)\n )\n .subscribe(([, { title, head }]) => {\n document.title = title\n\n /* Replace meta tags */\n for (const selector of [\n `link[rel=\"canonical\"]`,\n `meta[name=\"author\"]`,\n `meta[name=\"description\"]`\n ]) {\n const next = getElement(selector, head)\n const prev = getElement(selector, document.head)\n if (\n typeof next !== \"undefined\" &&\n typeof prev !== \"undefined\"\n ) {\n replaceElement(prev, next)\n }\n }\n\n /* Finished, dispatch document switch event */\n document.dispatchEvent(new CustomEvent(\"DOMContentSwitch\"))\n })\n\n /* Debounce update of viewport offset */\n viewport$\n .pipe(\n debounceTime(250),\n distinctUntilKeyChanged(\"offset\")\n )\n .subscribe(({ offset }) => {\n history.replaceState(offset, \"\")\n })\n\n /* Set viewport offset from history */\n merge(state$, pop$)\n .pipe(\n bufferCount(2, 1),\n filter(([prev, next]) => {\n return prev.url.pathname === next.url.pathname\n && !isAnchorLocation(next.url)\n }),\n map(([, state]) => state)\n )\n .subscribe(({ offset }) => {\n setViewportOffset(offset || { y: 0 })\n })\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable } from \"rxjs\"\nimport {\n filter,\n map,\n share,\n withLatestFrom\n} from \"rxjs/operators\"\n\nimport {\n Key,\n getActiveElement,\n getElement,\n getElements,\n getToggle,\n isSusceptibleToKeyboard,\n setElementFocus,\n setElementSelection,\n setToggle,\n watchKeyboard\n} from \"browser\"\nimport { useComponent } from \"components\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Keyboard mode\n */\nexport type KeyboardMode =\n | \"global\" /* Global */\n | \"search\" /* Search is open */\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Keyboard\n */\nexport interface Keyboard extends Key {\n mode: KeyboardMode /* Keyboard mode */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up keyboard\n *\n * This function will set up the keyboard handlers and ensure that keys are\n * correctly propagated. Currently there are two modes:\n *\n * - `global`: This mode is active when the search is closed. It is intended\n * to assign hotkeys to specific functions of the site. Currently the search,\n * previous and next page can be triggered.\n *\n * - `search`: This mode is active when the search is open. It maps certain\n * navigational keys to offer search results that can be entirely navigated\n * through keyboard input.\n *\n * The keyboard observable is returned and can be used to monitor the keyboard\n * in order toassign further hotkeys to custom functions.\n *\n * @return Keyboard observable\n */\nexport function setupKeyboard(): Observable {\n const keyboard$ = watchKeyboard()\n .pipe(\n map(key => ({\n mode: getToggle(\"search\") ? \"search\" : \"global\",\n ...key\n })),\n filter(({ mode }) => {\n if (mode === \"global\") {\n const active = getActiveElement()\n if (typeof active !== \"undefined\")\n return !isSusceptibleToKeyboard(active)\n }\n return true\n }),\n share()\n )\n\n /* Set up search keyboard handlers */\n keyboard$\n .pipe(\n filter(({ mode }) => mode === \"search\"),\n withLatestFrom(\n useComponent(\"search-query\"),\n useComponent(\"search-result\")\n )\n )\n .subscribe(([key, query, result]) => {\n const active = getActiveElement()\n switch (key.type) {\n\n /* Enter: prevent form submission */\n case \"Enter\":\n if (active === query)\n key.claim()\n break\n\n /* Escape or Tab: close search */\n case \"Escape\":\n case \"Tab\":\n setToggle(\"search\", false)\n setElementFocus(query, false)\n break\n\n /* Vertical arrows: select previous or next search result */\n case \"ArrowUp\":\n case \"ArrowDown\":\n if (typeof active === \"undefined\") {\n setElementFocus(query)\n } else {\n const els = [query, ...getElements(\n \":not(details) > [href], summary, details[open] [href]\",\n result\n )]\n const i = Math.max(0, (\n Math.max(0, els.indexOf(active)) + els.length + (\n key.type === \"ArrowUp\" ? -1 : +1\n )\n ) % els.length)\n setElementFocus(els[i])\n }\n\n /* Prevent scrolling of page */\n key.claim()\n break\n\n /* All other keys: hand to search query */\n default:\n if (query !== getActiveElement())\n setElementFocus(query)\n }\n })\n\n /* Set up global keyboard handlers */\n keyboard$\n .pipe(\n filter(({ mode }) => mode === \"global\"),\n withLatestFrom(useComponent(\"search-query\"))\n )\n .subscribe(([key, query]) => {\n switch (key.type) {\n\n /* Open search and select query */\n case \"f\":\n case \"s\":\n case \"/\":\n setElementFocus(query)\n setElementSelection(query)\n key.claim()\n break\n\n /* Go to previous page */\n case \"p\":\n case \",\":\n const prev = getElement(\"[href][rel=prev]\")\n if (typeof prev !== \"undefined\")\n prev.click()\n break\n\n /* Go to next page */\n case \"n\":\n case \".\":\n const next = getElement(\"[href][rel=next]\")\n if (typeof next !== \"undefined\")\n next.click()\n break\n }\n })\n\n /* Return keyboard */\n return keyboard$\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { EMPTY, Observable, of } from \"rxjs\"\nimport {\n distinctUntilChanged,\n map,\n scan,\n shareReplay,\n switchMap\n} from \"rxjs/operators\"\n\nimport { getElement, replaceElement } from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Component\n */\nexport type Component =\n | \"announce\" /* Announcement bar */\n | \"container\" /* Container */\n | \"header\" /* Header */\n | \"header-title\" /* Header title */\n | \"main\" /* Main area */\n | \"navigation\" /* Navigation */\n | \"search\" /* Search */\n | \"search-query\" /* Search input */\n | \"search-reset\" /* Search reset */\n | \"search-result\" /* Search results */\n | \"skip\" /* Skip link */\n | \"tabs\" /* Tabs */\n | \"toc\" /* Table of contents */\n\n/**\n * Component map\n */\nexport type ComponentMap = {\n [P in Component]?: HTMLElement\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Component map observable\n */\nlet components$: Observable\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up bindings to components with given names\n *\n * This function will maintain bindings to the elements identified by the given\n * names in-between document switches and update the elements in-place.\n *\n * @param names - Component names\n * @param options - Options\n */\nexport function setupComponents(\n names: Component[], { document$ }: WatchOptions\n): void {\n components$ = document$\n .pipe(\n\n /* Build component map */\n map(document => names.reduce((components, name) => {\n const el = getElement(`[data-md-component=${name}]`, document)\n return {\n ...components,\n ...typeof el !== \"undefined\" ? { [name]: el } : {}\n }\n }, {})),\n\n /* Re-compute component map on document switch */\n scan((prev, next) => {\n for (const name of names) {\n switch (name) {\n\n /* Top-level components: update */\n case \"announce\":\n case \"header-title\":\n case \"container\":\n case \"skip\":\n if (name in prev && typeof prev[name] !== \"undefined\") {\n replaceElement(prev[name]!, next[name]!)\n prev[name] = next[name]\n }\n break\n\n /* All other components: rebind */\n default:\n if (typeof next[name] !== \"undefined\")\n prev[name] = getElement(`[data-md-component=${name}]`)\n else\n delete prev[name]\n }\n }\n return prev\n }),\n\n /* Convert to hot observable */\n shareReplay({ bufferSize: 1, refCount: true })\n )\n}\n\n/**\n * Retrieve a component\n *\n * The returned observable will only re-emit if the element changed, i.e. if\n * it was replaced from a document which was switched to.\n *\n * @template T - Element type\n *\n * @param name - Component name\n *\n * @return Component observable\n */\nexport function useComponent(\n name: \"search-query\"\n): Observable\nexport function useComponent(\n name: Component\n): Observable\nexport function useComponent(\n name: Component\n): Observable {\n return components$\n .pipe(\n switchMap(components => (\n typeof components[name] !== \"undefined\"\n ? of(components[name] as T)\n : EMPTY\n )),\n distinctUntilChanged()\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set anchor blur\n *\n * @param el - Anchor element\n * @param value - Whether the anchor is blurred\n */\nexport function setAnchorBlur(\n el: HTMLElement, value: boolean\n): void {\n el.setAttribute(\"data-md-state\", value ? \"blur\" : \"\")\n}\n\n/**\n * Reset anchor blur\n *\n * @param el - Anchor element\n */\nexport function resetAnchorBlur(\n el: HTMLElement\n): void {\n el.removeAttribute(\"data-md-state\")\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Set anchor active\n *\n * @param el - Anchor element\n * @param value - Whether the anchor is active\n */\nexport function setAnchorActive(\n el: HTMLElement, value: boolean\n): void {\n el.classList.toggle(\"md-nav__link--active\", value)\n}\n\n/**\n * Reset anchor active\n *\n * @param el - Anchor element\n */\nexport function resetAnchorActive(\n el: HTMLElement\n): void {\n el.classList.remove(\"md-nav__link--active\")\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nexport * from \"./sidebar\"\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n SearchDocument,\n SearchMetadata,\n SearchResult\n} from \"integrations/search\"\nimport { h, translate, truncate } from \"utilities\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Render flag\n */\nconst enum Flag {\n TEASER = 1, /* Render teaser */\n PARENT = 2 /* Render as parent */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper function\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a search document\n *\n * @param section - Search document\n * @param flag - Render flags\n *\n * @return Element\n */\nfunction renderSearchDocument(\n document: SearchDocument & SearchMetadata, flag: Flag\n) {\n const parent = flag & Flag.PARENT\n const teaser = flag & Flag.TEASER\n\n /* Render missing query terms */\n const missing = Object.keys(document.terms)\n .filter(key => !document.terms[key])\n .map(key => [{key}, \" \"])\n .flat()\n .slice(0, -1)\n\n /* Render article or section, depending on flags */\n const url = document.location\n return (\n \n \n {parent > 0 &&
}\n

{document.title}

\n {teaser > 0 && document.text.length > 0 &&\n

\n {truncate(document.text, 320)}\n

\n }\n {teaser > 0 && missing.length > 0 &&\n

\n {translate(\"search.result.term.missing\")}: {...missing}\n

\n }\n \n
\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a search result\n *\n * @param result - Search result\n * @param threshold - Score threshold\n *\n * @return Element\n */\nexport function renderSearchResult(\n result: SearchResult, threshold: number = Infinity\n) {\n const docs = [...result]\n\n /* Find and extract parent article */\n const parent = docs.findIndex(doc => !doc.location.includes(\"#\"))\n const [article] = docs.splice(parent, 1)\n\n /* Determine last index above threshold */\n let index = docs.findIndex(doc => doc.score < threshold)\n if (index === -1)\n index = docs.length\n\n /* Partition sections */\n const best = docs.slice(0, index)\n const more = docs.slice(index)\n\n /* Render children */\n const children = [\n renderSearchDocument(article, Flag.PARENT | +(!parent && index === 0)),\n ...best.map(section => renderSearchDocument(section, Flag.TEASER)),\n ...more.length ? [\n
\n \n {more.length > 0 && more.length === 1\n ? translate(\"search.result.more.one\")\n : translate(\"search.result.more.other\", more.length)\n }\n \n {...more.map(section => renderSearchDocument(section, Flag.TEASER))}\n
\n ] : []\n ]\n\n /* Render search result */\n return (\n
  • \n {children}\n
  • \n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { h, translate } from \"utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a 'copy-to-clipboard' button\n *\n * @param id - Unique identifier\n *\n * @return Element\n */\nexport function renderClipboardButton(id: string) {\n return (\n code`}\n >\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SourceFacts } from \"patches/source\"\nimport { h } from \"utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render source facts\n *\n * @param facts - Source facts\n *\n * @return Element\n */\nexport function renderSource(\n facts: SourceFacts\n) {\n return (\n
      \n {facts.map(fact => (\n
    • {fact}
    • \n ))}\n
    \n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { h } from \"utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Render a table inside a wrapper to improve scrolling on mobile\n *\n * @param table - Table element\n *\n * @return Element\n */\nexport function renderTable(\n table: HTMLTableElement\n) {\n return (\n
    \n
    \n {table}\n
    \n
    \n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set sidebar offset\n *\n * @param el - Sidebar element\n * @param value - Sidebar offset\n */\nexport function setSidebarOffset(\n el: HTMLElement, value: number\n): void {\n el.style.top = `${value}px`\n}\n\n/**\n * Reset sidebar offset\n *\n * @param el - Sidebar element\n */\nexport function resetSidebarOffset(\n el: HTMLElement\n): void {\n el.style.top = \"\"\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Set sidebar height\n *\n * @param el - Sidebar element\n * @param value - Sidebar height\n */\nexport function setSidebarHeight(\n el: HTMLElement, value: number\n): void {\n el.style.height = `${value}px`\n}\n\n/**\n * Reset sidebar height\n *\n * @param el - Sidebar element\n */\nexport function resetSidebarHeight(\n el: HTMLElement\n): void {\n el.style.height = \"\"\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nexport * from \"./_\"\nexport * from \"./react\"\nexport * from \"./set\"\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search transformation function\n *\n * @param value - Query value\n *\n * @return Transformed query value\n */\nexport type SearchTransformFn = (value: string) => string\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Default transformation function\n *\n * 1. Search for terms in quotation marks and prepend a `+` modifier to denote\n * that the resulting document must contain all terms, converting the query\n * to an `AND` query (as opposed to the default `OR` behavior). While users\n * may expect terms enclosed in quotation marks to map to span queries, i.e.\n * for which order is important, `lunr` doesn't support them, so the best\n * we can do is to convert the terms to an `AND` query.\n *\n * 2. Replace control characters which are not located at the beginning of the\n * query or preceded by white space, or are not followed by a non-whitespace\n * character or are at the end of the query string. Furthermore, filter\n * unmatched quotation marks.\n *\n * 3. Trim excess whitespace from left and right.\n *\n * @param query - Query value\n *\n * @return Transformed query value\n */\nexport function defaultTransform(query: string): string {\n return query\n .split(/\"([^\"]+)\"/g) /* => 1 */\n .map((terms, index) => index & 1\n ? terms.replace(/^\\b|^(?![^\\x00-\\x7F]|$)|\\s+/g, \" +\")\n : terms\n )\n .join(\"\")\n .replace(/\"|(?:^|\\s+)[*+\\-:^~]+(?=\\s+|$)/g, \"\") /* => 2 */\n .trim() /* => 3 */\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SearchIndex, SearchResult } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search message type\n */\nexport const enum SearchMessageType {\n SETUP, /* Search index setup */\n READY, /* Search index ready */\n QUERY, /* Search query */\n RESULT /* Search results */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * A message containing the data necessary to setup the search index\n */\nexport interface SearchSetupMessage {\n type: SearchMessageType.SETUP /* Message type */\n data: SearchIndex /* Message data */\n}\n\n/**\n * A message indicating the search index is ready\n */\nexport interface SearchReadyMessage {\n type: SearchMessageType.READY /* Message type */\n}\n\n/**\n * A message containing a search query\n */\nexport interface SearchQueryMessage {\n type: SearchMessageType.QUERY /* Message type */\n data: string /* Message data */\n}\n\n/**\n * A message containing results for a search query\n */\nexport interface SearchResultMessage {\n type: SearchMessageType.RESULT /* Message type */\n data: SearchResult[] /* Message data */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * A message exchanged with the search worker\n */\nexport type SearchMessage =\n | SearchSetupMessage\n | SearchReadyMessage\n | SearchQueryMessage\n | SearchResultMessage\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Type guard for search setup messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchSetupMessage(\n message: SearchMessage\n): message is SearchSetupMessage {\n return message.type === SearchMessageType.SETUP\n}\n\n/**\n * Type guard for search ready messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchReadyMessage(\n message: SearchMessage\n): message is SearchReadyMessage {\n return message.type === SearchMessageType.READY\n}\n\n/**\n * Type guard for search query messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchQueryMessage(\n message: SearchMessage\n): message is SearchQueryMessage {\n return message.type === SearchMessageType.QUERY\n}\n\n/**\n * Type guard for search result messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchResultMessage(\n message: SearchMessage\n): message is SearchResultMessage {\n return message.type === SearchMessageType.RESULT\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, Subject, asyncScheduler } from \"rxjs\"\nimport {\n map,\n observeOn,\n share,\n withLatestFrom\n} from \"rxjs/operators\"\n\nimport { WorkerHandler, watchWorker } from \"browser\"\nimport { translate } from \"utilities\"\n\nimport { SearchIndex, SearchIndexPipeline } from \"../../_\"\nimport {\n SearchMessage,\n SearchMessageType,\n SearchSetupMessage,\n isSearchResultMessage\n} from \"../message\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Setup options\n */\ninterface SetupOptions {\n index$: Observable /* Search index observable */\n base$: Observable /* Location base observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up search index\n *\n * @param data - Search index\n *\n * @return Search index\n */\nfunction setupSearchIndex(\n { config, docs, index }: SearchIndex\n): SearchIndex {\n\n /* Override default language with value from translation */\n if (config.lang.length === 1 && config.lang[0] === \"en\")\n config.lang = [translate(\"search.config.lang\")]\n\n /* Override default separator with value from translation */\n if (config.separator === \"[\\\\s\\\\-]+\")\n config.separator = translate(\"search.config.separator\")\n\n /* Set pipeline from translation */\n const pipeline = translate(\"search.config.pipeline\")\n .split(/\\s*,\\s*/)\n .filter(Boolean) as SearchIndexPipeline\n\n /* Return search index after defaulting */\n return { config, docs, index, pipeline }\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set up search web worker\n *\n * This function will create a web worker to set up and query the search index\n * which is done using `lunr`. The index must be passed as an observable to\n * enable hacks like _localsearch_ via search index embedding as JSON.\n *\n * @param url - Worker URL\n * @param options - Options\n *\n * @return Worker handler\n */\nexport function setupSearchWorker(\n url: string, { index$, base$ }: SetupOptions\n): WorkerHandler {\n const worker = new Worker(url)\n\n /* Create communication channels and resolve relative links */\n const tx$ = new Subject()\n const rx$ = watchWorker(worker, { tx$ })\n .pipe(\n withLatestFrom(base$),\n map(([message, base]) => {\n if (isSearchResultMessage(message)) {\n for (const result of message.data)\n for (const document of result)\n document.location = `${base}/${document.location}`\n }\n return message\n }),\n share()\n )\n\n /* Set up search index */\n index$\n .pipe(\n map(data => ({\n type: SearchMessageType.SETUP,\n data: setupSearchIndex(data)\n })),\n observeOn(asyncScheduler)\n )\n .subscribe(tx$.next.bind(tx$))\n\n /* Return worker handler */\n return { tx$, rx$ }\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, OperatorFunction, of, pipe } from \"rxjs\"\nimport { map, switchMap } from \"rxjs/operators\"\n\nimport { Viewport } from \"browser\"\n\nimport { Header } from \"../header\"\nimport { Main } from \"../main\"\nimport {\n Sidebar,\n applySidebar,\n watchSidebar\n} from \"../shared\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Navigation for [screen -]\n */\ninterface NavigationBelowScreen {} // tslint:disable-line\n\n/**\n * Navigation for [screen +]\n */\ninterface NavigationAboveScreen {\n sidebar: Sidebar /* Sidebar */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Navigation\n */\nexport type Navigation =\n | NavigationBelowScreen\n | NavigationAboveScreen\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n header$: Observable
    /* Header observable */\n main$: Observable
    /* Main area observable */\n viewport$: Observable /* Viewport observable */\n screen$: Observable /* Screen media observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount navigation from source observable\n *\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountNavigation(\n { header$, main$, viewport$, screen$ }: MountOptions\n): OperatorFunction {\n return pipe(\n switchMap(el => screen$\n .pipe(\n switchMap(screen => {\n\n /* [screen +]: Mount navigation in sidebar */\n if (screen) {\n return watchSidebar(el, { main$, viewport$ })\n .pipe(\n applySidebar(el, { header$ }),\n map(sidebar => ({ sidebar }))\n )\n\n /* [screen -]: Mount navigation in drawer */\n } else {\n return of({})\n }\n })\n )\n )\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nexport * from \"./_\"\nexport * from \"./react\"\nexport * from \"./set\"\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n MonoTypeOperatorFunction,\n Observable,\n animationFrameScheduler,\n combineLatest,\n pipe\n} from \"rxjs\"\nimport {\n distinctUntilChanged,\n finalize,\n map,\n observeOn,\n tap,\n withLatestFrom\n} from \"rxjs/operators\"\n\nimport { Viewport } from \"browser\"\n\nimport { Header } from \"../../../header\"\nimport { Main } from \"../../../main\"\nimport { Sidebar } from \"../_\"\nimport {\n resetSidebarHeight,\n resetSidebarOffset,\n setSidebarHeight,\n setSidebarOffset\n} from \"../set\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n main$: Observable
    /* Main area observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/**\n * Apply options\n */\ninterface ApplyOptions {\n header$: Observable
    /* Header observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch sidebar\n *\n * This function returns an observable that computes the visual parameters of\n * the sidebar which depends on the vertical viewport offset, as well as the\n * height of the main area. When the page is scrolled beyond the header, the\n * sidebar is locked and fills the remaining space.\n *\n * @param el - Sidebar element\n * @param options - Options\n *\n * @return Sidebar observable\n */\nexport function watchSidebar(\n el: HTMLElement, { main$, viewport$ }: WatchOptions\n): Observable {\n const adjust = el.parentElement!.offsetTop\n - el.parentElement!.parentElement!.offsetTop\n\n /* Compute the sidebar's available height and if it should be locked */\n return combineLatest([main$, viewport$])\n .pipe(\n map(([{ offset, height }, { offset: { y } }]) => {\n height = height\n + Math.min(adjust, Math.max(0, y - offset))\n - adjust\n return {\n height,\n lock: y >= offset + adjust\n }\n }),\n distinctUntilChanged((a, b) => {\n return a.height === b.height\n && a.lock === b.lock\n })\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Apply sidebar\n *\n * @param el - Sidebar element\n * @param options - Options\n *\n * @return Operator function\n */\nexport function applySidebar(\n el: HTMLElement, { header$ }: ApplyOptions\n): MonoTypeOperatorFunction {\n return pipe(\n\n /* Defer repaint to next animation frame */\n observeOn(animationFrameScheduler),\n withLatestFrom(header$),\n tap(([{ height, lock }, { height: offset }]) => {\n setSidebarHeight(el, height)\n\n /* Set offset in locked state depending on header height */\n if (lock)\n setSidebarOffset(el, offset)\n else\n resetSidebarOffset(el)\n }),\n\n /* Re-map to sidebar */\n map(([sidebar]) => sidebar),\n\n /* Reset on complete or error */\n finalize(() => {\n resetSidebarOffset(el)\n resetSidebarHeight(el)\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nexport * from \"./_\"\nexport * from \"./anchor\"\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n OperatorFunction,\n combineLatest,\n of,\n pipe\n} from \"rxjs\"\nimport { map, switchMap } from \"rxjs/operators\"\n\nimport { Viewport, getElements } from \"browser\"\n\nimport { Header } from \"../../header\"\nimport { Main } from \"../../main\"\nimport {\n Sidebar,\n applySidebar,\n watchSidebar\n} from \"../../shared\"\nimport {\n AnchorList,\n applyAnchorList,\n watchAnchorList\n} from \"../anchor\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Table of contents for [tablet -]\n */\ninterface TableOfContentsBelowTablet {} // tslint:disable-line\n\n/**\n * Table of contents for [tablet +]\n */\ninterface TableOfContentsAboveTablet {\n sidebar: Sidebar /* Sidebar */\n anchors: AnchorList /* Anchor list */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Table of contents\n */\nexport type TableOfContents =\n | TableOfContentsBelowTablet\n | TableOfContentsAboveTablet\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n header$: Observable
    /* Header observable */\n main$: Observable
    /* Main area observable */\n viewport$: Observable /* Viewport observable */\n tablet$: Observable /* Tablet media observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount table of contents from source observable\n *\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountTableOfContents(\n { header$, main$, viewport$, tablet$ }: MountOptions\n): OperatorFunction {\n return pipe(\n switchMap(el => tablet$\n .pipe(\n switchMap(tablet => {\n\n /* [tablet +]: Mount table of contents in sidebar */\n if (tablet) {\n const els = getElements(\".md-nav__link\", el)\n\n /* Watch and apply sidebar */\n const sidebar$ = watchSidebar(el, { main$, viewport$ })\n .pipe(\n applySidebar(el, { header$ })\n )\n\n /* Watch and apply anchor list (scroll spy) */\n const anchors$ = watchAnchorList(els, { header$, viewport$ })\n .pipe(\n applyAnchorList(els)\n )\n\n /* Combine into single hot observable */\n return combineLatest([sidebar$, anchors$])\n .pipe(\n map(([sidebar, anchors]) => ({ sidebar, anchors }))\n )\n\n /* [tablet -]: Unmount table of contents */\n } else {\n return of({})\n }\n })\n )\n )\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n MonoTypeOperatorFunction,\n Observable,\n animationFrameScheduler,\n combineLatest,\n pipe\n} from \"rxjs\"\nimport {\n bufferCount,\n distinctUntilChanged,\n distinctUntilKeyChanged,\n finalize,\n map,\n observeOn,\n scan,\n startWith,\n switchMap,\n tap\n} from \"rxjs/operators\"\n\nimport { Viewport, getElement, watchElementSize } from \"browser\"\n\nimport { Header } from \"../../../header\"\nimport { AnchorList } from \"../_\"\nimport {\n resetAnchorActive,\n resetAnchorBlur,\n setAnchorActive,\n setAnchorBlur\n} from \"../set\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n header$: Observable
    /* Header observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch anchor list\n *\n * This is effectively a scroll-spy implementation which will account for the\n * fixed header and automatically re-calculate anchor offsets when the viewport\n * is resized. The returned observable will only emit if the anchor list needs\n * to be repainted.\n *\n * This implementation tracks an anchor element's entire path starting from its\n * level up to the top-most anchor element, e.g. `[h3, h2, h1]`. Although the\n * Material theme currently doesn't make use of this information, it enables\n * the styling of the entire hierarchy through customization.\n *\n * Note that the current anchor is the last item of the `prev` anchor list.\n *\n * @param els - Anchor elements\n * @param options - Options\n *\n * @return Anchor list observable\n */\nexport function watchAnchorList(\n els: HTMLAnchorElement[], { header$, viewport$ }: WatchOptions\n): Observable {\n const table = new Map()\n for (const el of els) {\n const id = decodeURIComponent(el.hash.substring(1))\n const target = getElement(`[id=\"${id}\"]`)\n if (typeof target !== \"undefined\")\n table.set(el, target)\n }\n\n /* Compute necessary adjustment for header */\n const adjust$ = header$\n .pipe(\n map(header => 18 + header.height)\n )\n\n /* Compute partition of previous and next anchors */\n const partition$ = watchElementSize(document.body)\n .pipe(\n distinctUntilKeyChanged(\"height\"),\n\n /* Build index to map anchor paths to vertical offsets */\n map(() => {\n let path: HTMLAnchorElement[] = []\n return [...table].reduce((index, [anchor, target]) => {\n while (path.length) {\n const last = table.get(path[path.length - 1])!\n if (last.tagName >= target.tagName) {\n path.pop()\n } else {\n break\n }\n }\n\n /* If the current anchor is hidden, continue with its parent */\n let offset = target.offsetTop\n while (!offset && target.parentElement) {\n target = target.parentElement\n offset = target.offsetTop\n }\n\n /* Map reversed anchor path to vertical offset */\n return index.set(\n [...path = [...path, anchor]].reverse(),\n offset\n )\n }, new Map())\n }),\n\n /* Re-compute partition when viewport offset changes */\n switchMap(index => combineLatest([adjust$, viewport$])\n .pipe(\n scan(([prev, next], [adjust, { offset: { y } }]) => {\n\n /* Look forward */\n while (next.length) {\n const [, offset] = next[0]\n if (offset - adjust < y) {\n prev = [...prev, next.shift()!]\n } else {\n break\n }\n }\n\n /* Look backward */\n while (prev.length) {\n const [, offset] = prev[prev.length - 1]\n if (offset - adjust >= y) {\n next = [prev.pop()!, ...next]\n } else {\n break\n }\n }\n\n /* Return partition */\n return [prev, next]\n }, [[], [...index]]),\n distinctUntilChanged((a, b) => {\n return a[0] === b[0]\n && a[1] === b[1]\n })\n )\n )\n )\n\n /* Compute and return anchor list migrations */\n return partition$\n .pipe(\n map(([prev, next]) => ({\n prev: prev.map(([path]) => path),\n next: next.map(([path]) => path)\n })),\n\n /* Extract anchor list migrations */\n startWith({ prev: [], next: [] }),\n bufferCount(2, 1),\n map(([a, b]) => {\n\n /* Moving down */\n if (a.prev.length < b.prev.length) {\n return {\n prev: b.prev.slice(Math.max(0, a.prev.length - 1), b.prev.length),\n next: []\n }\n\n /* Moving up */\n } else {\n return {\n prev: b.prev.slice(-1),\n next: b.next.slice(0, b.next.length - a.next.length)\n }\n }\n })\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Apply anchor list\n *\n * @param els - Anchor elements\n *\n * @return Operator function\n */\nexport function applyAnchorList(\n els: HTMLAnchorElement[]\n): MonoTypeOperatorFunction {\n return pipe(\n\n /* Defer repaint to next animation frame */\n observeOn(animationFrameScheduler),\n tap(({ prev, next }) => {\n\n /* Look forward */\n for (const [el] of next) {\n resetAnchorActive(el)\n resetAnchorBlur(el)\n }\n\n /* Look backward */\n prev.forEach(([el], index) => {\n setAnchorActive(el, index === prev.length - 1)\n setAnchorBlur(el, true)\n })\n }),\n\n /* Reset on complete or error */\n finalize(() => {\n for (const el of els) {\n resetAnchorActive(el)\n resetAnchorBlur(el)\n }\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, OperatorFunction, combineLatest, pipe } from \"rxjs\"\nimport {\n filter,\n map,\n mapTo,\n sample,\n startWith,\n switchMap,\n take\n} from \"rxjs/operators\"\n\nimport { WorkerHandler } from \"browser\"\nimport {\n SearchMessage,\n SearchResult,\n isSearchQueryMessage,\n isSearchReadyMessage\n} from \"integrations/search\"\n\nimport { SearchQuery } from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search status\n */\nexport type SearchStatus =\n | \"waiting\" /* Search waiting for initialization */\n | \"ready\" /* Search ready */\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search\n */\nexport interface Search {\n status: SearchStatus /* Search status */\n query: SearchQuery /* Search query */\n result: SearchResult[] /* Search result list */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n query$: Observable /* Search query observable */\n reset$: Observable /* Search reset observable */\n result$: Observable /* Search result observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search from source observable\n *\n * @param handler - Worker handler\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountSearch(\n { rx$, tx$ }: WorkerHandler,\n { query$, reset$, result$ }: MountOptions\n): OperatorFunction {\n return pipe(\n switchMap(() => {\n\n /* Compute search status */\n const status$ = rx$\n .pipe(\n filter(isSearchReadyMessage),\n mapTo(\"ready\"),\n startWith(\"waiting\")\n ) as Observable\n\n /* Re-emit the latest query when search is ready */\n tx$\n .pipe(\n filter(isSearchQueryMessage),\n sample(status$),\n take(1)\n )\n .subscribe(tx$.next.bind(tx$))\n\n /* Combine into single observable */\n return combineLatest([status$, query$, result$, reset$])\n .pipe(\n map(([status, query, result]) => ({\n status,\n query,\n result\n }))\n )\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { OperatorFunction, pipe } from \"rxjs\"\nimport {\n distinctUntilKeyChanged,\n map,\n switchMap\n} from \"rxjs/operators\"\n\nimport { WorkerHandler, setToggle } from \"browser\"\nimport {\n SearchMessage,\n SearchMessageType,\n SearchQueryMessage,\n SearchTransformFn\n} from \"integrations\"\n\nimport { watchSearchQuery } from \"../react\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search query\n */\nexport interface SearchQuery {\n value: string /* Query value */\n focus: boolean /* Query focus */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n transform?: SearchTransformFn /* Transformation function */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search query from source observable\n *\n * @param handler - Worker handler\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountSearchQuery(\n { tx$ }: WorkerHandler, options: MountOptions = {}\n): OperatorFunction {\n return pipe(\n switchMap(el => {\n const query$ = watchSearchQuery(el, options)\n\n /* Subscribe worker to search query */\n query$\n .pipe(\n distinctUntilKeyChanged(\"value\"),\n map(({ value }): SearchQueryMessage => ({\n type: SearchMessageType.QUERY,\n data: value\n }))\n )\n .subscribe(tx$.next.bind(tx$))\n\n /* Toggle search on focus */\n query$\n .pipe(\n distinctUntilKeyChanged(\"focus\")\n )\n .subscribe(({ focus }) => {\n if (focus)\n setToggle(\"search\", focus)\n })\n\n /* Return search query */\n return query$\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, combineLatest, fromEvent, merge } from \"rxjs\"\nimport {\n delay,\n distinctUntilChanged,\n map,\n startWith\n} from \"rxjs/operators\"\n\nimport { watchElementFocus } from \"browser\"\nimport { SearchTransformFn, defaultTransform } from \"integrations\"\n\nimport { SearchQuery } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n transform?: SearchTransformFn /* Transformation function */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch search query\n *\n * Note that the focus event which triggers re-reading the current query value\n * is delayed by `1ms` so the input's empty state is allowed to propagate.\n *\n * @param el - Search query element\n * @param options - Options\n *\n * @return Search query observable\n */\nexport function watchSearchQuery(\n el: HTMLInputElement, { transform }: WatchOptions = {}\n): Observable {\n const fn = transform || defaultTransform\n\n /* Intercept keyboard events */\n const value$ = merge(\n fromEvent(el, \"keyup\"),\n fromEvent(el, \"focus\").pipe(delay(1))\n )\n .pipe(\n map(() => fn(el.value)),\n startWith(fn(el.value)),\n distinctUntilChanged()\n )\n\n /* Intercept focus events */\n const focus$ = watchElementFocus(el)\n\n /* Combine into single observable */\n return combineLatest([value$, focus$])\n .pipe(\n map(([value, focus]) => ({ value, focus }))\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { OperatorFunction, pipe } from \"rxjs\"\nimport {\n mapTo,\n startWith,\n switchMap,\n switchMapTo,\n tap\n} from \"rxjs/operators\"\n\nimport { setElementFocus } from \"browser\"\n\nimport { useComponent } from \"../../../_\"\nimport { watchSearchReset } from \"../react\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search reset from source observable\n *\n * @return Operator function\n */\nexport function mountSearchReset(): OperatorFunction {\n return pipe(\n switchMap(el => watchSearchReset(el)\n .pipe(\n switchMapTo(useComponent(\"search-query\")),\n tap(setElementFocus),\n mapTo(undefined)\n )\n ),\n startWith(undefined)\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent } from \"rxjs\"\nimport { mapTo } from \"rxjs/operators\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch search reset\n *\n * @param el - Search reset element\n *\n * @return Search reset observable\n */\nexport function watchSearchReset(\n el: HTMLElement\n): Observable {\n return fromEvent(el, \"click\")\n .pipe(\n mapTo(undefined)\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { translate } from \"utilities\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set number of search results\n *\n * @param el - Search result metadata element\n * @param value - Number of results\n */\nexport function setSearchResultMeta(\n el: HTMLElement, value: number\n): void {\n switch (value) {\n\n /* No results */\n case 0:\n el.textContent = translate(\"search.result.none\")\n break\n\n /* One result */\n case 1:\n el.textContent = translate(\"search.result.one\")\n break\n\n /* Multiple result */\n default:\n el.textContent = translate(\"search.result.other\", value)\n }\n}\n\n/**\n * Reset number of search results\n *\n * @param el - Search result metadata element\n */\nexport function resetSearchResultMeta(\n el: HTMLElement\n): void {\n el.textContent = translate(\"search.result.placeholder\")\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Add an element to the search result list\n *\n * @param el - Search result list element\n * @param child - Search result element\n */\nexport function addToSearchResultList(\n el: HTMLElement, child: Element\n): void {\n el.appendChild(child)\n}\n\n/**\n * Reset search result list\n *\n * @param el - Search result list element\n */\nexport function resetSearchResultList(\n el: HTMLElement\n): void {\n el.innerHTML = \"\"\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n MonoTypeOperatorFunction,\n Observable,\n animationFrameScheduler,\n pipe\n} from \"rxjs\"\nimport {\n finalize,\n map,\n mapTo,\n observeOn,\n scan,\n switchMap,\n withLatestFrom\n} from \"rxjs/operators\"\n\nimport { getElementOrThrow } from \"browser\"\nimport { SearchResult } from \"integrations/search\"\nimport { renderSearchResult } from \"templates\"\n\nimport { SearchQuery } from \"../../query\"\nimport {\n addToSearchResultList,\n resetSearchResultList,\n resetSearchResultMeta,\n setSearchResultMeta\n} from \"../set\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Apply options\n */\ninterface ApplyOptions {\n query$: Observable /* Search query observable */\n ready$: Observable /* Search ready observable */\n fetch$: Observable /* Result fetch observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Apply search results\n *\n * This function will perform a lazy rendering of the search results, depending\n * on the vertical offset of the search result container. When the scroll offset\n * reaches the bottom of the element, more results are fetched and rendered.\n *\n * @param el - Search result element\n * @param options - Options\n *\n * @return Operator function\n */\nexport function applySearchResult(\n el: HTMLElement, { query$, ready$, fetch$ }: ApplyOptions\n): MonoTypeOperatorFunction {\n const list = getElementOrThrow(\".md-search-result__list\", el)\n const meta = getElementOrThrow(\".md-search-result__meta\", el)\n return pipe(\n\n /* Apply search result metadata */\n withLatestFrom(query$, ready$),\n map(([result, query]) => {\n if (query.value) {\n setSearchResultMeta(meta, result.length)\n } else {\n resetSearchResultMeta(meta)\n }\n return result\n }),\n\n /* Apply search result list */\n switchMap(result => {\n const thresholds = [...result.map(([best]) => best.score), 0]\n return fetch$\n .pipe(\n\n /* Defer repaint to next animation frame */\n observeOn(animationFrameScheduler),\n scan(index => {\n const container = el.parentElement!\n while (index < result.length) {\n addToSearchResultList(list, renderSearchResult(\n result[index++], thresholds[index]\n ))\n if (container.scrollHeight - container.offsetHeight > 16)\n break\n }\n return index\n }, 0),\n\n /* Re-map to search result */\n mapTo(result),\n\n /* Reset on complete or error */\n finalize(() => {\n resetSearchResultList(list)\n })\n )\n }\n )\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, OperatorFunction, pipe } from \"rxjs\"\nimport {\n distinctUntilChanged,\n filter,\n map,\n mapTo,\n startWith,\n switchMap\n} from \"rxjs/operators\"\n\nimport { WorkerHandler, watchElementOffset } from \"browser\"\nimport {\n SearchMessage,\n SearchResult,\n isSearchReadyMessage,\n isSearchResultMessage\n} from \"integrations\"\n\nimport { SearchQuery } from \"../../query\"\nimport { applySearchResult } from \"../react\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n query$: Observable /* Search query observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount search result from source observable\n *\n * @param handler - Worker handler\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountSearchResult(\n { rx$ }: WorkerHandler, { query$ }: MountOptions\n): OperatorFunction {\n return pipe(\n switchMap(el => {\n const container = el.parentElement!\n\n /* Compute if search is ready */\n const ready$ = rx$\n .pipe(\n filter(isSearchReadyMessage),\n mapTo(true)\n )\n\n /* Compute whether there are more search results to fetch */\n const fetch$ = watchElementOffset(container)\n .pipe(\n map(({ y }) => {\n return y >= container.scrollHeight - container.offsetHeight - 16\n }),\n distinctUntilChanged(),\n filter(Boolean)\n )\n\n /* Apply search results */\n return rx$\n .pipe(\n filter(isSearchResultMessage),\n map(({ data }) => data),\n applySearchResult(el, { query$, ready$, fetch$ }),\n startWith([])\n )\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, OperatorFunction, combineLatest, pipe } from \"rxjs\"\nimport {\n distinctUntilChanged,\n filter,\n map,\n startWith,\n switchMap,\n zipWith\n} from \"rxjs/operators\"\n\nimport {\n Viewport,\n getElement,\n watchViewportAt\n} from \"browser\"\n\nimport { useComponent } from \"../../_\"\nimport {\n applyHeaderType,\n watchHeader\n} from \"../react\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Header type\n */\nexport type HeaderType =\n | \"site\" /* Header shows site title */\n | \"page\" /* Header shows page title */\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Header\n */\nexport interface Header {\n type: HeaderType /* Header type */\n sticky: boolean /* Header stickyness */\n height: number /* Header visible height */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n document$: Observable /* Document observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount header from source observable\n *\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountHeader(\n { document$, viewport$ }: MountOptions\n): OperatorFunction {\n return pipe(\n switchMap(el => {\n const header$ = watchHeader(el, { document$ })\n\n /* Compute whether the header should switch to page header */\n const type$ = useComponent(\"main\")\n .pipe(\n map(main => getElement(\"h1, h2, h3, h4, h5, h6\", main)!),\n filter(hx => typeof hx !== \"undefined\"),\n zipWith(useComponent(\"header-title\")),\n switchMap(([hx, title]) => watchViewportAt(hx, { header$, viewport$ })\n .pipe(\n map(({ offset: { y } }) => {\n return y >= hx.offsetHeight ? \"page\" : \"site\"\n }),\n distinctUntilChanged(),\n applyHeaderType(title)\n )\n ),\n startWith(\"site\")\n )\n\n /* Combine into single observable */\n return combineLatest([header$, type$])\n .pipe(\n map(([header, type]): Header => ({ type, ...header }))\n )\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n MonoTypeOperatorFunction,\n Observable,\n animationFrameScheduler,\n of,\n pipe\n} from \"rxjs\"\nimport {\n distinctUntilChanged,\n finalize,\n map,\n observeOn,\n shareReplay,\n switchMap,\n tap\n} from \"rxjs/operators\"\n\nimport { watchElementSize } from \"browser\"\n\nimport { Header, HeaderType } from \"../_\"\nimport {\n resetHeaderTitleActive,\n setHeaderTitleActive\n} from \"../set\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch header\n *\n * @param el - Header element\n *\n * @return Header observable\n */\nexport function watchHeader(\n el: HTMLElement, { document$ }: WatchOptions\n): Observable> {\n return document$\n .pipe(\n map(() => {\n const styles = getComputedStyle(el)\n return [\n \"sticky\", /* Modern browsers */\n \"-webkit-sticky\" /* Safari */\n ].includes(styles.position)\n }),\n distinctUntilChanged(),\n switchMap(sticky => {\n if (sticky) {\n return watchElementSize(el)\n .pipe(\n map(({ height }) => ({\n sticky: true,\n height\n }))\n )\n } else {\n return of({\n sticky: false,\n height: 0\n })\n }\n }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Apply header title type\n *\n * @param el - Header title element\n *\n * @return Operator function\n */\nexport function applyHeaderType(\n el: HTMLElement\n): MonoTypeOperatorFunction {\n return pipe(\n\n /* Defer repaint to next animation frame */\n observeOn(animationFrameScheduler),\n tap(type => {\n setHeaderTitleActive(el, type === \"page\")\n }),\n\n /* Reset on complete or error */\n finalize(() => {\n resetHeaderTitleActive(el)\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set header title active\n *\n * @param el - Header title element\n * @param value - Whether the title is shown\n */\nexport function setHeaderTitleActive(\n el: HTMLElement, value: boolean\n): void {\n el.setAttribute(\"data-md-state\", value ? \"active\" : \"\")\n}\n\n/**\n * Reset header title active\n *\n * @param el - Header title element\n */\nexport function resetHeaderTitleActive(\n el: HTMLElement\n): void {\n el.removeAttribute(\"data-md-state\")\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n Observable,\n OperatorFunction,\n Subject,\n noop,\n pipe\n} from \"rxjs\"\nimport {\n distinctUntilKeyChanged,\n finalize,\n switchMap,\n tap\n} from \"rxjs/operators\"\n\nimport { Viewport } from \"browser\"\n\nimport { useComponent } from \"../../_\"\nimport { Header } from \"../../header\"\nimport {\n applyHeaderShadow,\n watchMain\n} from \"../react\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Main area\n */\nexport interface Main {\n offset: number /* Main area top offset */\n height: number /* Main area visible height */\n active: boolean /* Scrolled past top offset */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n header$: Observable
    /* Header observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount main area from source observable\n *\n * The header must be connected to the main area observable outside of the\n * operator function, as the header will persist in-between document switches\n * while the main area is replaced. However, the header observable must be\n * passed to this function, so we connect both via a long-living subject.\n *\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountMain(\n { header$, viewport$ }: MountOptions\n): OperatorFunction {\n const main$ = new Subject
    ()\n\n /* Connect to main area observable via long-living subject */\n useComponent(\"header\")\n .pipe(\n switchMap(header => main$\n .pipe(\n distinctUntilKeyChanged(\"active\"),\n applyHeaderShadow(header)\n )\n )\n )\n .subscribe(noop)\n\n /* Return operator */\n return pipe(\n switchMap(el => watchMain(el, { header$, viewport$ })),\n tap(main => main$.next(main)),\n finalize(() => main$.complete())\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n MonoTypeOperatorFunction,\n Observable,\n animationFrameScheduler,\n combineLatest,\n pipe\n} from \"rxjs\"\nimport {\n distinctUntilChanged,\n distinctUntilKeyChanged,\n finalize,\n map,\n observeOn,\n switchMap,\n tap\n} from \"rxjs/operators\"\n\nimport { Viewport, watchElementSize } from \"browser\"\n\nimport { Header } from \"../../header\"\nimport { Main } from \"../_\"\nimport {\n resetHeaderShadow,\n setHeaderShadow\n} from \"../set\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch options\n */\ninterface WatchOptions {\n header$: Observable
    /* Header observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Watch main area\n *\n * This function returns an observable that computes the visual parameters of\n * the main area which depends on the viewport vertical offset and height, as\n * well as the height of the header element, if the header is fixed.\n *\n * @param el - Main area element\n * @param options - Options\n *\n * @return Main area observable\n */\nexport function watchMain(\n el: HTMLElement, { header$, viewport$ }: WatchOptions\n): Observable
    {\n\n /* Compute necessary adjustment for header */\n const adjust$ = header$\n .pipe(\n map(({ height }) => height),\n distinctUntilChanged()\n )\n\n /* Compute the main area's top and bottom borders */\n const border$ = adjust$\n .pipe(\n switchMap(() => watchElementSize(el)\n .pipe(\n map(({ height }) => ({\n top: el.offsetTop,\n bottom: el.offsetTop + height\n })),\n distinctUntilKeyChanged(\"bottom\")\n )\n )\n )\n\n /* Compute the main area's offset, visible height and if we scrolled past */\n return combineLatest([adjust$, border$, viewport$])\n .pipe(\n map(([header, { top, bottom }, { offset: { y }, size: { height } }]) => {\n height = Math.max(0, height\n - Math.max(0, top - y, header)\n - Math.max(0, height + y - bottom)\n )\n return {\n offset: top - header,\n height,\n active: top - header <= y\n }\n }),\n distinctUntilChanged
    ((a, b) => {\n return a.offset === b.offset\n && a.height === b.height\n && a.active === b.active\n })\n )\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Apply header shadow\n *\n * @param el - Header element\n *\n * @return Operator function\n */\nexport function applyHeaderShadow(\n el: HTMLElement\n): MonoTypeOperatorFunction
    {\n return pipe(\n\n /* Defer repaint to next animation frame */\n observeOn(animationFrameScheduler),\n tap(({ active }) => {\n setHeaderShadow(el, active)\n }),\n\n /* Reset on complete or error */\n finalize(() => {\n resetHeaderShadow(el)\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set header shadow\n *\n * @param el - Header element\n * @param value - Whether the shadow is shown\n */\nexport function setHeaderShadow(\n el: HTMLElement, value: boolean\n): void {\n el.setAttribute(\"data-md-state\", value ? \"shadow\" : \"\")\n}\n\n/**\n * Reset header shadow\n *\n * @param el - Header element\n */\nexport function resetHeaderShadow(\n el: HTMLElement\n): void {\n el.removeAttribute(\"data-md-state\")\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, OperatorFunction, of, pipe } from \"rxjs\"\nimport {\n distinctUntilKeyChanged,\n map,\n switchMap\n} from \"rxjs/operators\"\n\nimport { Viewport, watchViewportAt } from \"browser\"\n\nimport { Header } from \"../../header\"\nimport { applyTabs } from \"../react\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Tabs\n */\nexport interface Tabs {\n hidden: boolean /* Whether the tabs are hidden */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n header$: Observable
    /* Header observable */\n viewport$: Observable /* Viewport observable */\n screen$: Observable /* Media screen observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount tabs from source observable\n *\n * @param options - Options\n *\n * @return Operator function\n */\nexport function mountTabs(\n { header$, viewport$, screen$ }: MountOptions\n): OperatorFunction {\n return pipe(\n switchMap(el => screen$\n .pipe(\n switchMap(screen => {\n\n /* [screen +]: Mount tabs above screen breakpoint */\n if (screen) {\n return watchViewportAt(el, { header$, viewport$ })\n .pipe(\n map(({ offset: { y } }) => ({ hidden: y >= 10 })),\n distinctUntilKeyChanged(\"hidden\"),\n applyTabs(el)\n )\n\n /* [screen -]: Unmount tabs below screen breakpoint */\n } else {\n return of({ hidden: true })\n }\n })\n )\n )\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n MonoTypeOperatorFunction,\n animationFrameScheduler,\n pipe\n} from \"rxjs\"\nimport { finalize, observeOn, tap } from \"rxjs/operators\"\n\nimport { Tabs } from \"../_\"\nimport {\n resetTabsHidden,\n setTabsHidden\n} from \"../set\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Apply tabs\n *\n * @param el - Tabs element\n *\n * @return Operator function\n */\nexport function applyTabs(\n el: HTMLElement\n): MonoTypeOperatorFunction {\n return pipe(\n\n /* Defer repaint to next animation frame */\n observeOn(animationFrameScheduler),\n tap(({ hidden }) => {\n setTabsHidden(el, hidden)\n }),\n\n /* Reset on complete or error */\n finalize(() => {\n resetTabsHidden(el)\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Set tabs hidden\n *\n * @param el - Tabs element\n * @param value - Whether the element is hidden\n */\nexport function setTabsHidden(\n el: HTMLElement, value: boolean\n): void {\n el.setAttribute(\"data-md-state\", value ? \"hidden\" : \"\")\n}\n\n/**\n * Reset tabs hidden\n *\n * @param el - Tabs element\n */\nexport function resetTabsHidden(\n el: HTMLElement\n): void {\n el.removeAttribute(\"data-md-state\")\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { NEVER, Observable, fromEvent, iif, merge } from \"rxjs\"\nimport { map, mapTo, shareReplay, switchMap } from \"rxjs/operators\"\n\nimport { getElements } from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Check whether the given device is an Apple device\n *\n * @return Test result\n */\nfunction isAppleDevice(): boolean {\n return /(iPad|iPhone|iPod)/.test(navigator.userAgent)\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch all elements with `data-md-scrollfix` attributes\n *\n * This is a year-old patch which ensures that overflow scrolling works at the\n * top and bottom of containers on iOS by ensuring a `1px` scroll offset upon\n * the start of a touch event.\n *\n * @see https://bit.ly/2SCtAOO - Original source\n *\n * @param options - Options\n */\nexport function patchScrollfix(\n { document$ }: PatchOptions\n): void {\n const els$ = document$\n .pipe(\n map(() => getElements(\"[data-md-scrollfix]\")),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n /* Remove marker attribute, so we'll only add the fix once */\n els$.subscribe(els => {\n for (const el of els)\n el.removeAttribute(\"data-md-scrollfix\")\n })\n\n /* Patch overflow scrolling on touch start */\n iif(isAppleDevice, els$, NEVER)\n .pipe(\n switchMap(els => merge(...els.map(el => (\n fromEvent(el, \"touchstart\")\n .pipe(\n mapTo(el)\n )\n ))))\n )\n .subscribe(el => {\n const top = el.scrollTop\n\n /* We're at the top of the container */\n if (top === 0) {\n el.scrollTop = 1\n\n /* We're at the bottom of the container */\n } else if (top + el.offsetHeight === el.scrollHeight) {\n el.scrollTop = top - 1\n }\n })\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { NEVER, Observable } from \"rxjs\"\nimport { catchError, map, switchMap } from \"rxjs/operators\"\n\nimport { getElementOrThrow, getElements } from \"browser\"\nimport { renderSource } from \"templates\"\nimport { cache, hash } from \"utilities\"\n\nimport { fetchSourceFactsFromGitHub } from \"./github\"\nimport { fetchSourceFactsFromGitLab } from \"./gitlab\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Source facts\n */\nexport type SourceFacts = string[]\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch source facts\n *\n * @param url - Source repository URL\n *\n * @return Source facts observable\n */\nfunction fetchSourceFacts(\n url: string\n): Observable {\n const [type] = url.match(/(git(?:hub|lab))/i) || []\n switch (type.toLowerCase()) {\n\n /* GitHub repository */\n case \"github\":\n const [, user, repo] = url.match(/^.+github\\.com\\/([^\\/]+)\\/?([^\\/]+)?/i)\n return fetchSourceFactsFromGitHub(user, repo)\n\n /* GitLab repository */\n case \"gitlab\":\n const [, base, slug] = url.match(/^.+?([^\\/]*gitlab[^\\/]+)\\/(.+?)\\/?$/i)\n return fetchSourceFactsFromGitLab(base, slug)\n\n /* Everything else */\n default:\n return NEVER\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch elements containing repository information\n *\n * This function will retrieve the URL from the repository link and try to\n * query data from integrated source code platforms like GitHub or GitLab.\n *\n * @param options - Options\n */\nexport function patchSource(\n { document$ }: PatchOptions\n): void {\n document$\n .pipe(\n map(() => getElementOrThrow(\".md-source[href]\")),\n switchMap(({ href }) => (\n cache(`${hash(href)}`, () => fetchSourceFacts(href))\n )),\n catchError(() => NEVER)\n )\n .subscribe(facts => {\n for (const el of getElements(\".md-source__repository\")) {\n if (!el.hasAttribute(\"data-md-state\")) {\n el.setAttribute(\"data-md-state\", \"done\")\n el.appendChild(renderSource(facts))\n }\n }\n })\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Repo, User } from \"github-types\"\nimport { Observable, from } from \"rxjs\"\nimport { map } from \"rxjs/operators\"\n\nimport { round } from \"utilities\"\n\nimport { SourceFacts } from \"..\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch GitHub source facts\n *\n * @param user - GitHub user\n * @param repo - GitHub repository\n *\n * @return Source facts observable\n */\nexport function fetchSourceFactsFromGitHub(\n user: string, repo?: string\n): Observable {\n const url = typeof repo !== \"undefined\"\n ? `https://api.github.com/repos/${user}/${repo}`\n : `https://api.github.com/users/${user}`\n return from(fetch(url).then(res => res.json()))\n .pipe(\n map(data => {\n\n /* GitHub repository */\n if (typeof repo !== \"undefined\") {\n const { stargazers_count, forks_count }: Repo = data\n return [\n `${round(stargazers_count || 0)} Stars`,\n `${round(forks_count || 0)} Forks`\n ]\n\n /* GitHub user/organization */\n } else {\n const { public_repos }: User = data\n return [\n `${round(public_repos || 0)} Repositories`\n ]\n }\n })\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { ProjectSchema } from \"gitlab\"\nimport { Observable, from } from \"rxjs\"\nimport { map } from \"rxjs/operators\"\n\nimport { round } from \"utilities\"\n\nimport { SourceFacts } from \"..\"\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch GitLab source facts\n *\n * @param base - GitLab base\n * @param project - GitLab project\n *\n * @return Source facts observable\n */\nexport function fetchSourceFactsFromGitLab(\n base: string, project: string\n): Observable {\n const url = `https://${base}/api/v4/projects/${encodeURIComponent(project)}`\n return from(fetch(url).then(res => res.json()))\n .pipe(\n map(({ star_count, forks_count }: ProjectSchema) => ([\n `${round(star_count)} Stars`,\n `${round(forks_count)} Forks`\n ]))\n )\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n// DISCLAIMER: this file is still WIP. There're some refactoring opportunities\n// which must be tackled after we gathered some feedback on v5.\n// tslint:disable\n\nimport \"focus-visible\"\n\nimport {\n merge,\n combineLatest,\n animationFrameScheduler,\n fromEvent,\n from,\n defer,\n of,\n NEVER\n} from \"rxjs\"\nimport {\n delay,\n switchMap,\n tap,\n filter,\n withLatestFrom,\n observeOn,\n take,\n shareReplay,\n catchError,\n map\n} from \"rxjs/operators\"\n\nimport {\n watchToggle,\n setToggle,\n getElements,\n watchMedia,\n watchDocument,\n watchLocation,\n watchLocationHash,\n watchViewport,\n isLocalLocation,\n setLocationHash,\n watchLocationBase\n} from \"browser\"\nimport {\n mountHeader,\n mountMain,\n mountNavigation,\n mountSearch,\n mountTableOfContents,\n mountTabs,\n useComponent,\n setupComponents,\n mountSearchQuery,\n mountSearchReset,\n mountSearchResult\n} from \"components\"\nimport {\n setupClipboard,\n setupDialog,\n setupKeyboard,\n setupInstantLoading,\n setupSearchWorker,\n SearchIndex,\n SearchIndexPipeline\n} from \"integrations\"\nimport {\n patchCodeBlocks,\n patchTables,\n patchDetails,\n patchScrollfix,\n patchSource,\n patchScripts\n} from \"patches\"\nimport { isConfig } from \"utilities\"\n\n/* ------------------------------------------------------------------------- */\n\n/* Denote that JavaScript is available */\ndocument.documentElement.classList.remove(\"no-js\")\ndocument.documentElement.classList.add(\"js\")\n\n/* Test for iOS */\nif (navigator.userAgent.match(/(iPad|iPhone|iPod)/g))\n document.documentElement.classList.add(\"ios\")\n\n/**\n * Set scroll lock\n *\n * @param el - Scrollable element\n * @param value - Vertical offset\n */\nexport function setScrollLock(\n el: HTMLElement, value: number\n): void {\n el.setAttribute(\"data-md-state\", \"lock\")\n el.style.top = `-${value}px`\n}\n\n/**\n * Reset scroll lock\n *\n * @param el - Scrollable element\n */\nexport function resetScrollLock(\n el: HTMLElement\n): void {\n const value = -1 * parseInt(el.style.top, 10)\n el.removeAttribute(\"data-md-state\")\n el.style.top = \"\"\n if (value)\n window.scrollTo(0, value)\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Initialize Material for MkDocs\n *\n * @param config - Configuration\n */\nexport function initialize(config: unknown) {\n if (!isConfig(config))\n throw new SyntaxError(`Invalid configuration: ${JSON.stringify(config)}`)\n\n /* Set up subjects */\n const document$ = watchDocument()\n const location$ = watchLocation()\n\n /* Set up user interface observables */\n const base$ = watchLocationBase(config.base, { location$ })\n const hash$ = watchLocationHash()\n const viewport$ = watchViewport()\n const tablet$ = watchMedia(\"(min-width: 960px)\")\n const screen$ = watchMedia(\"(min-width: 1220px)\")\n\n /* ----------------------------------------------------------------------- */\n\n /* Set up component bindings */\n setupComponents([\n \"announce\", /* Announcement bar */\n \"container\", /* Container */\n \"header\", /* Header */\n \"header-title\", /* Header title */\n \"main\", /* Main area */\n \"navigation\", /* Navigation */\n \"search\", /* Search */\n \"search-query\", /* Search input */\n \"search-reset\", /* Search reset */\n \"search-result\", /* Search results */\n \"skip\", /* Skip link */\n \"tabs\", /* Tabs */\n \"toc\" /* Table of contents */\n ], { document$ })\n\n const keyboard$ = setupKeyboard()\n\n // Hack: only make code blocks focusable on non-touch devices\n if (matchMedia(\"(hover)\").matches)\n patchCodeBlocks({ document$, viewport$ })\n patchDetails({ document$, hash$ })\n patchScripts({ document$ })\n patchSource({ document$ })\n patchTables({ document$ })\n\n /* Force 1px scroll offset to trigger overflow scrolling */\n patchScrollfix({ document$ })\n\n /* Set up clipboard and dialog */\n const dialog$ = setupDialog()\n const clipboard$ = setupClipboard({ document$, dialog$ })\n\n /* ----------------------------------------------------------------------- */\n\n /* Create header observable */\n const header$ = useComponent(\"header\")\n .pipe(\n mountHeader({ document$, viewport$ }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n const main$ = useComponent(\"main\")\n .pipe(\n mountMain({ header$, viewport$ }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n /* ----------------------------------------------------------------------- */\n\n const navigation$ = useComponent(\"navigation\")\n .pipe(\n mountNavigation({ header$, main$, viewport$, screen$ }),\n shareReplay({ bufferSize: 1, refCount: true }) // shareReplay because there might be late subscribers\n )\n\n const toc$ = useComponent(\"toc\")\n .pipe(\n mountTableOfContents({ header$, main$, viewport$, tablet$ }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n const tabs$ = useComponent(\"tabs\")\n .pipe(\n mountTabs({ header$, viewport$, screen$ }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n /* ----------------------------------------------------------------------- */\n\n /* Search worker - only if search is present */\n const worker$ = useComponent(\"search\")\n .pipe(\n switchMap(() => defer(() => {\n const index = config.search && config.search.index\n ? config.search.index\n : undefined\n\n /* Fetch index if it wasn't passed explicitly */\n const index$ = (\n typeof index !== \"undefined\"\n ? from(index)\n : base$\n .pipe(\n switchMap(base => fetch(`${base}/search/search_index.json`, {\n credentials: \"same-origin\"\n }).then(res => res.json())) // SearchIndex\n )\n )\n\n return of(setupSearchWorker(config.search.worker, {\n base$, index$\n }))\n }))\n )\n\n /* ----------------------------------------------------------------------- */\n\n /* Mount search query */\n const search$ = worker$\n .pipe(\n switchMap(worker => {\n\n const query$ = useComponent(\"search-query\")\n .pipe(\n mountSearchQuery(worker, { transform: config.search.transform }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n /* Mount search reset */\n const reset$ = useComponent(\"search-reset\")\n .pipe(\n mountSearchReset(),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n /* Mount search result */\n const result$ = useComponent(\"search-result\")\n .pipe(\n mountSearchResult(worker, { query$ }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n return useComponent(\"search\")\n .pipe(\n mountSearch(worker, { query$, reset$, result$ }),\n )\n }),\n catchError(() => {\n useComponent(\"search\")\n .subscribe(el => el.hidden = true) // TODO: Hack\n return NEVER\n }),\n shareReplay({ bufferSize: 1, refCount: true })\n )\n\n /* ----------------------------------------------------------------------- */\n\n // // put into search...\n hash$\n .pipe(\n tap(() => setToggle(\"search\", false)),\n delay(125), // ensure that it runs after the body scroll reset...\n )\n .subscribe(hash => setLocationHash(`#${hash}`))\n\n // TODO: scroll restoration must be centralized\n combineLatest([\n watchToggle(\"search\"),\n tablet$,\n ])\n .pipe(\n withLatestFrom(viewport$),\n switchMap(([[toggle, tablet], { offset: { y }}]) => {\n const active = toggle && !tablet\n return document$\n .pipe(\n delay(active ? 400 : 100),\n observeOn(animationFrameScheduler),\n tap(({ body }) => active\n ? setScrollLock(body, y)\n : resetScrollLock(body)\n )\n )\n })\n )\n .subscribe()\n\n /* ----------------------------------------------------------------------- */\n\n /* Always close drawer on click */\n fromEvent(document.body, \"click\")\n .pipe(\n filter(ev => !(ev.metaKey || ev.ctrlKey)),\n filter(ev => {\n if (ev.target instanceof HTMLElement) {\n const el = ev.target.closest(\"a\") // TODO: abstract as link click?\n if (el && isLocalLocation(el)) {\n return true\n }\n }\n return false\n })\n )\n .subscribe(() => {\n setToggle(\"drawer\", false)\n })\n\n /* Enable instant loading, if not on file:// protocol */\n if (\n config.features.includes(\"navigation.instant\") &&\n location.protocol !== \"file:\"\n ) {\n const dom = new DOMParser()\n\n /* Fetch sitemap and extract URL whitelist */\n base$\n .pipe(\n switchMap(base => from(fetch(`${base}/sitemap.xml`)\n .then(res => res.text())\n .then(text => dom.parseFromString(text, \"text/xml\"))\n )),\n withLatestFrom(base$),\n map(([document, base]) => {\n const urls = getElements(\"loc\", document)\n .map(node => node.textContent!)\n\n // Hack: This is a temporary fix to normalize instant loading lookup\n // on localhost and Netlify previews. If this approach proves to be\n // suitable, we'll refactor URL whitelisting anyway. We take the two\n // shortest URLs and determine the common prefix to isolate the\n // domain. If there're no two domains, we just leave it as-is, as\n // there isn't anything to be loaded anway.\n if (urls.length > 1) {\n const [a, b] = urls.sort((a, b) => a.length - b.length)\n\n /* Determine common prefix */\n let index = 0\n if (a === b)\n index = a.length\n else\n while (a.charAt(index) === b.charAt(index))\n index++\n\n /* Replace common prefix (i.e. base) with effective base */\n for (let i = 0; i < urls.length; i++)\n urls[i] = urls[i].replace(a.slice(0, index), `${base}/`)\n }\n return urls\n })\n )\n .subscribe(urls => {\n setupInstantLoading(urls, { document$, location$, viewport$ })\n })\n }\n\n /* ----------------------------------------------------------------------- */\n\n /* Unhide permalinks on first tab */\n keyboard$\n .pipe(\n filter(key => key.mode === \"global\" && key.type === \"Tab\"),\n take(1)\n )\n .subscribe(() => {\n for (const link of getElements(\".headerlink\"))\n link.style.visibility = \"visible\"\n })\n\n /* ----------------------------------------------------------------------- */\n\n const state = {\n\n /* Browser observables */\n document$,\n location$,\n viewport$,\n\n /* Component observables */\n header$,\n main$,\n navigation$,\n search$,\n tabs$,\n toc$,\n\n /* Integration observables */\n clipboard$,\n keyboard$,\n dialog$\n }\n\n /* Subscribe to all observables */\n merge(...Object.values(state))\n .subscribe()\n return state\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, combineLatest } from \"rxjs\"\nimport { distinctUntilKeyChanged, map } from \"rxjs/operators\"\n\nimport { Viewport, getElements } from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n document$: Observable /* Document observable */\n viewport$: Observable /* Viewport observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch all `code` elements\n *\n * This function will make overflowing code blocks focusable via keyboard, so\n * they can be scrolled without a mouse.\n *\n * @param options - Options\n */\nexport function patchCodeBlocks(\n { document$, viewport$ }: MountOptions\n): void {\n const els$ = document$\n .pipe(\n map(() => getElements(\"pre > code\"))\n )\n\n /* Observe viewport size only */\n const size$ = viewport$\n .pipe(\n distinctUntilKeyChanged(\"size\")\n )\n\n /* Make overflowing elements focusable */\n combineLatest([els$, size$])\n .subscribe(([els]) => {\n for (const el of els) {\n if (el.scrollWidth > el.clientWidth)\n el.setAttribute(\"tabindex\", \"0\")\n else\n el.removeAttribute(\"tabindex\")\n }\n })\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable, fromEvent, merge } from \"rxjs\"\nimport {\n filter,\n map,\n switchMapTo,\n tap\n} from \"rxjs/operators\"\n\nimport {\n getElement,\n getElements,\n watchMedia\n} from \"browser\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n document$: Observable /* Document observable */\n hash$: Observable /* Location hash observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch all `details` elements\n *\n * This function will ensure that all `details` tags are opened prior to\n * printing, so the whole content of the page is included, and on anchor jumps.\n *\n * @param options - Options\n */\nexport function patchDetails(\n { document$, hash$ }: PatchOptions\n): void {\n const els$ = document$\n .pipe(\n map(() => getElements(\"details\"))\n )\n\n /* Open all details before printing */\n merge(\n watchMedia(\"print\").pipe(filter(Boolean)), /* Webkit */\n fromEvent(window, \"beforeprint\") /* IE, FF */\n )\n .pipe(\n switchMapTo(els$)\n )\n .subscribe(els => {\n for (const el of els)\n el.setAttribute(\"open\", \"\")\n })\n\n /* Open parent details and fix anchor jump */\n hash$\n .pipe(\n map(id => getElement(`[id=\"${id}\"]`)!),\n filter(el => typeof el !== \"undefined\"),\n tap(el => {\n const details = el.closest(\"details\")\n if (details && !details.open)\n details.setAttribute(\"open\", \"\")\n })\n )\n .subscribe(el => el.scrollIntoView())\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { EMPTY, Observable, noop, of } from \"rxjs\"\nimport {\n concatMap,\n map,\n skip,\n switchMap,\n withLatestFrom\n} from \"rxjs/operators\"\n\nimport {\n createElement,\n getElements,\n replaceElement\n} from \"browser\"\nimport { useComponent } from \"components\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch options\n */\ninterface PatchOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch all `script` elements\n *\n * This function must be run after a document switch, which means the first\n * emission must be ignored.\n *\n * @param options - Options\n */\nexport function patchScripts(\n { document$ }: PatchOptions\n): void {\n const els$ = document$\n .pipe(\n skip(1),\n withLatestFrom(useComponent(\"container\")),\n map(([, el]) => getElements(\"script\", el))\n )\n\n /* Evaluate all scripts via replacement in order */\n els$\n .pipe(\n switchMap(els => of(...els)),\n concatMap(el => {\n const script = createElement(\"script\")\n if (el.src) {\n script.src = el.src\n replaceElement(el, script)\n\n /* Complete when script is loaded */\n return new Observable(observer => {\n script.onload = () => observer.complete()\n })\n\n /* Complete immediately */\n } else {\n script.textContent = el.textContent!\n replaceElement(el, script)\n return EMPTY\n }\n })\n )\n .subscribe(noop)\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { Observable } from \"rxjs\"\nimport { map } from \"rxjs/operators\"\n\nimport {\n createElement,\n getElements,\n replaceElement\n} from \"browser\"\nimport { renderTable } from \"templates\"\n\n/* ----------------------------------------------------------------------------\n * Helper types\n * ------------------------------------------------------------------------- */\n\n/**\n * Mount options\n */\ninterface MountOptions {\n document$: Observable /* Document observable */\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Patch all `table` elements\n *\n * This function will re-render all tables by wrapping them to improve overflow\n * scrolling on smaller screen sizes.\n *\n * @param options - Options\n */\nexport function patchTables(\n { document$ }: MountOptions\n): void {\n const sentinel = createElement(\"table\")\n document$\n .pipe(\n map(() => getElements(\"table:not([class])\"))\n )\n .subscribe(els => {\n for (const el of els) {\n replaceElement(el, sentinel)\n replaceElement(sentinel, renderTable(el))\n }\n })\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.ar.min.js b/v2/assets/javascripts/lunr/min/lunr.ar.min.js new file mode 100644 index 000000000..248ddc5d1 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.ar.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ar=function(){this.pipeline.reset(),this.pipeline.add(e.ar.trimmer,e.ar.stopWordFilter,e.ar.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ar.stemmer))},e.ar.wordCharacters="ء-ٛٱـ",e.ar.trimmer=e.trimmerSupport.generateTrimmer(e.ar.wordCharacters),e.Pipeline.registerFunction(e.ar.trimmer,"trimmer-ar"),e.ar.stemmer=function(){var e=this;return e.result=!1,e.preRemoved=!1,e.sufRemoved=!1,e.pre={pre1:"ف ك ب و س ل ن ا ي ت",pre2:"ال لل",pre3:"بال وال فال تال كال ولل",pre4:"فبال كبال وبال وكال"},e.suf={suf1:"ه ك ت ن ا ي",suf2:"نك نه ها وك يا اه ون ين تن تم نا وا ان كم كن ني نن ما هم هن تك ته ات يه",suf3:"تين كهم نيه نهم ونه وها يهم ونا ونك وني وهم تكم تنا تها تني تهم كما كها ناه نكم هنا تان يها",suf4:"كموه ناها ونني ونهم تكما تموه تكاه كماه ناكم ناهم نيها وننا"},e.patterns=JSON.parse('{"pt43":[{"pt":[{"c":"ا","l":1}]},{"pt":[{"c":"ا,ت,ن,ي","l":0}],"mPt":[{"c":"ف","l":0,"m":1},{"c":"ع","l":1,"m":2},{"c":"ل","l":2,"m":3}]},{"pt":[{"c":"و","l":2}],"mPt":[{"c":"ف","l":0,"m":0},{"c":"ع","l":1,"m":1},{"c":"ل","l":2,"m":3}]},{"pt":[{"c":"ا","l":2}]},{"pt":[{"c":"ي","l":2}],"mPt":[{"c":"ف","l":0,"m":0},{"c":"ع","l":1,"m":1},{"c":"ا","l":2},{"c":"ل","l":3,"m":3}]},{"pt":[{"c":"م","l":0}]}],"pt53":[{"pt":[{"c":"ت","l":0},{"c":"ا","l":2}]},{"pt":[{"c":"ا,ن,ت,ي","l":0},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ت","l":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":0},{"c":"ا","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ع","l":2,"m":3},{"c":"ل","l":3,"m":4},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":0},{"c":"ا","l":3}],"mPt":[{"c":"ف","l":0,"m":1},{"c":"ع","l":1,"m":2},{"c":"ل","l":2,"m":4}]},{"pt":[{"c":"ا","l":3},{"c":"ن","l":4}]},{"pt":[{"c":"ت","l":0},{"c":"ي","l":3}]},{"pt":[{"c":"م","l":0},{"c":"و","l":3}]},{"pt":[{"c":"ا","l":1},{"c":"و","l":3}]},{"pt":[{"c":"و","l":1},{"c":"ا","l":2}]},{"pt":[{"c":"م","l":0},{"c":"ا","l":3}]},{"pt":[{"c":"م","l":0},{"c":"ي","l":3}]},{"pt":[{"c":"ا","l":2},{"c":"ن","l":3}]},{"pt":[{"c":"م","l":0},{"c":"ن","l":1}],"mPt":[{"c":"ا","l":0},{"c":"ن","l":1},{"c":"ف","l":2,"m":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"م","l":0},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ت","l":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"م","l":0},{"c":"ا","l":2}]},{"pt":[{"c":"م","l":1},{"c":"ا","l":3}]},{"pt":[{"c":"ي,ت,ا,ن","l":0},{"c":"ت","l":1}],"mPt":[{"c":"ف","l":0,"m":2},{"c":"ع","l":1,"m":3},{"c":"ا","l":2},{"c":"ل","l":3,"m":4}]},{"pt":[{"c":"ت,ي,ا,ن","l":0},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ت","l":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":2},{"c":"ي","l":3}]},{"pt":[{"c":"ا,ي,ت,ن","l":0},{"c":"ن","l":1}],"mPt":[{"c":"ا","l":0},{"c":"ن","l":1},{"c":"ف","l":2,"m":2},{"c":"ع","l":3,"m":3},{"c":"ا","l":4},{"c":"ل","l":5,"m":4}]},{"pt":[{"c":"ا","l":3},{"c":"ء","l":4}]}],"pt63":[{"pt":[{"c":"ا","l":0},{"c":"ت","l":2},{"c":"ا","l":4}]},{"pt":[{"c":"ا,ت,ن,ي","l":0},{"c":"س","l":1},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"س","l":1},{"c":"ت","l":2},{"c":"ف","l":3,"m":3},{"c":"ع","l":4,"m":4},{"c":"ا","l":5},{"c":"ل","l":6,"m":5}]},{"pt":[{"c":"ا,ن,ت,ي","l":0},{"c":"و","l":3}]},{"pt":[{"c":"م","l":0},{"c":"س","l":1},{"c":"ت","l":2}],"mPt":[{"c":"ا","l":0},{"c":"س","l":1},{"c":"ت","l":2},{"c":"ف","l":3,"m":3},{"c":"ع","l":4,"m":4},{"c":"ا","l":5},{"c":"ل","l":6,"m":5}]},{"pt":[{"c":"ي","l":1},{"c":"ي","l":3},{"c":"ا","l":4},{"c":"ء","l":5}]},{"pt":[{"c":"ا","l":0},{"c":"ن","l":1},{"c":"ا","l":4}]}],"pt54":[{"pt":[{"c":"ت","l":0}]},{"pt":[{"c":"ا,ي,ت,ن","l":0}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ع","l":2,"m":2},{"c":"ل","l":3,"m":3},{"c":"ر","l":4,"m":4},{"c":"ا","l":5},{"c":"ر","l":6,"m":4}]},{"pt":[{"c":"م","l":0}],"mPt":[{"c":"ا","l":0},{"c":"ف","l":1,"m":1},{"c":"ع","l":2,"m":2},{"c":"ل","l":3,"m":3},{"c":"ر","l":4,"m":4},{"c":"ا","l":5},{"c":"ر","l":6,"m":4}]},{"pt":[{"c":"ا","l":2}]},{"pt":[{"c":"ا","l":0},{"c":"ن","l":2}]}],"pt64":[{"pt":[{"c":"ا","l":0},{"c":"ا","l":4}]},{"pt":[{"c":"م","l":0},{"c":"ت","l":1}]}],"pt73":[{"pt":[{"c":"ا","l":0},{"c":"س","l":1},{"c":"ت","l":2},{"c":"ا","l":5}]}],"pt75":[{"pt":[{"c":"ا","l":0},{"c":"ا","l":5}]}]}'),e.execArray=["cleanWord","removeDiacritics","cleanAlef","removeStopWords","normalizeHamzaAndAlef","removeStartWaw","removePre432","removeEndTaa","wordCheck"],e.stem=function(){var r=0;for(e.result=!1,e.preRemoved=!1,e.sufRemoved=!1;r=0)return!0},e.normalizeHamzaAndAlef=function(){return e.word=e.word.replace("ؤ","ء"),e.word=e.word.replace("ئ","ء"),e.word=e.word.replace(/([\u0627])\1+/gi,"ا"),!1},e.removeEndTaa=function(){return!(e.word.length>2)||(e.word=e.word.replace(/[\u0627]$/,""),e.word=e.word.replace("ة",""),!1)},e.removeStartWaw=function(){return e.word.length>3&&"و"==e.word[0]&&"و"==e.word[1]&&(e.word=e.word.slice(1)),!1},e.removePre432=function(){var r=e.word;if(e.word.length>=7){var t=new RegExp("^("+e.pre.pre4.split(" ").join("|")+")");e.word=e.word.replace(t,"")}if(e.word==r&&e.word.length>=6){var c=new RegExp("^("+e.pre.pre3.split(" ").join("|")+")");e.word=e.word.replace(c,"")}if(e.word==r&&e.word.length>=5){var l=new RegExp("^("+e.pre.pre2.split(" ").join("|")+")");e.word=e.word.replace(l,"")}return r!=e.word&&(e.preRemoved=!0),!1},e.patternCheck=function(r){for(var t=0;t3){var t=new RegExp("^("+e.pre.pre1.split(" ").join("|")+")");e.word=e.word.replace(t,"")}return r!=e.word&&(e.preRemoved=!0),!1},e.removeSuf1=function(){var r=e.word;if(0==e.sufRemoved&&e.word.length>3){var t=new RegExp("("+e.suf.suf1.split(" ").join("|")+")$");e.word=e.word.replace(t,"")}return r!=e.word&&(e.sufRemoved=!0),!1},e.removeSuf432=function(){var r=e.word;if(e.word.length>=6){var t=new RegExp("("+e.suf.suf4.split(" ").join("|")+")$");e.word=e.word.replace(t,"")}if(e.word==r&&e.word.length>=5){var c=new RegExp("("+e.suf.suf3.split(" ").join("|")+")$");e.word=e.word.replace(c,"")}if(e.word==r&&e.word.length>=4){var l=new RegExp("("+e.suf.suf2.split(" ").join("|")+")$");e.word=e.word.replace(l,"")}return r!=e.word&&(e.sufRemoved=!0),!1},e.wordCheck=function(){for(var r=(e.word,[e.removeSuf432,e.removeSuf1,e.removePre1]),t=0,c=!1;e.word.length>=7&&!e.result&&t=f.limit)return;f.cursor++}for(;!f.out_grouping(w,97,248);){if(f.cursor>=f.limit)return;f.cursor++}d=f.cursor,d=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(c,32),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del();break;case 2:f.in_grouping_b(p,97,229)&&f.slice_del()}}function t(){var e,r=f.limit-f.cursor;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.find_among_b(l,4)?(f.bra=f.cursor,f.limit_backward=e,f.cursor=f.limit-r,f.cursor>f.limit_backward&&(f.cursor--,f.bra=f.cursor,f.slice_del())):f.limit_backward=e)}function s(){var e,r,i,n=f.limit-f.cursor;if(f.ket=f.cursor,f.eq_s_b(2,"st")&&(f.bra=f.cursor,f.eq_s_b(2,"ig")&&f.slice_del()),f.cursor=f.limit-n,f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(m,5),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del(),i=f.limit-f.cursor,t(),f.cursor=f.limit-i;break;case 2:f.slice_from("løs")}}function o(){var e;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.out_grouping_b(w,97,248)?(f.bra=f.cursor,u=f.slice_to(u),f.limit_backward=e,f.eq_v_b(u)&&f.slice_del()):f.limit_backward=e)}var a,d,u,c=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],l=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],w=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],p=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],f=new i;this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var r=f.cursor;return e(),f.limit_backward=r,f.cursor=f.limit,n(),f.cursor=f.limit,t(),f.cursor=f.limit,s(),f.cursor=f.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.de.min.js b/v2/assets/javascripts/lunr/min/lunr.de.min.js new file mode 100644 index 000000000..f3b5c108c --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.de.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `German` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.de=function(){this.pipeline.reset(),this.pipeline.add(e.de.trimmer,e.de.stopWordFilter,e.de.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.de.stemmer))},e.de.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.de.trimmer=e.trimmerSupport.generateTrimmer(e.de.wordCharacters),e.Pipeline.registerFunction(e.de.trimmer,"trimmer-de"),e.de.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(e,r,n){return!(!v.eq_s(1,e)||(v.ket=v.cursor,!v.in_grouping(p,97,252)))&&(v.slice_from(r),v.cursor=n,!0)}function i(){for(var r,n,i,s,t=v.cursor;;)if(r=v.cursor,v.bra=r,v.eq_s(1,"ß"))v.ket=v.cursor,v.slice_from("ss");else{if(r>=v.limit)break;v.cursor=r+1}for(v.cursor=t;;)for(n=v.cursor;;){if(i=v.cursor,v.in_grouping(p,97,252)){if(s=v.cursor,v.bra=s,e("u","U",i))break;if(v.cursor=s,e("y","Y",i))break}if(i>=v.limit)return void(v.cursor=n);v.cursor=i+1}}function s(){for(;!v.in_grouping(p,97,252);){if(v.cursor>=v.limit)return!0;v.cursor++}for(;!v.out_grouping(p,97,252);){if(v.cursor>=v.limit)return!0;v.cursor++}return!1}function t(){m=v.limit,l=m;var e=v.cursor+3;0<=e&&e<=v.limit&&(d=e,s()||(m=v.cursor,m=v.limit)return;v.cursor++}}}function c(){return m<=v.cursor}function u(){return l<=v.cursor}function a(){var e,r,n,i,s=v.limit-v.cursor;if(v.ket=v.cursor,(e=v.find_among_b(w,7))&&(v.bra=v.cursor,c()))switch(e){case 1:v.slice_del();break;case 2:v.slice_del(),v.ket=v.cursor,v.eq_s_b(1,"s")&&(v.bra=v.cursor,v.eq_s_b(3,"nis")&&v.slice_del());break;case 3:v.in_grouping_b(g,98,116)&&v.slice_del()}if(v.cursor=v.limit-s,v.ket=v.cursor,(e=v.find_among_b(f,4))&&(v.bra=v.cursor,c()))switch(e){case 1:v.slice_del();break;case 2:if(v.in_grouping_b(k,98,116)){var t=v.cursor-3;v.limit_backward<=t&&t<=v.limit&&(v.cursor=t,v.slice_del())}}if(v.cursor=v.limit-s,v.ket=v.cursor,(e=v.find_among_b(_,8))&&(v.bra=v.cursor,u()))switch(e){case 1:v.slice_del(),v.ket=v.cursor,v.eq_s_b(2,"ig")&&(v.bra=v.cursor,r=v.limit-v.cursor,v.eq_s_b(1,"e")||(v.cursor=v.limit-r,u()&&v.slice_del()));break;case 2:n=v.limit-v.cursor,v.eq_s_b(1,"e")||(v.cursor=v.limit-n,v.slice_del());break;case 3:if(v.slice_del(),v.ket=v.cursor,i=v.limit-v.cursor,!v.eq_s_b(2,"er")&&(v.cursor=v.limit-i,!v.eq_s_b(2,"en")))break;v.bra=v.cursor,c()&&v.slice_del();break;case 4:v.slice_del(),v.ket=v.cursor,e=v.find_among_b(b,2),e&&(v.bra=v.cursor,u()&&1==e&&v.slice_del())}}var d,l,m,h=[new r("",-1,6),new r("U",0,2),new r("Y",0,1),new r("ä",0,3),new r("ö",0,4),new r("ü",0,5)],w=[new r("e",-1,2),new r("em",-1,1),new r("en",-1,2),new r("ern",-1,1),new r("er",-1,1),new r("s",-1,3),new r("es",5,2)],f=[new r("en",-1,1),new r("er",-1,1),new r("st",-1,2),new r("est",2,1)],b=[new r("ig",-1,1),new r("lich",-1,1)],_=[new r("end",-1,1),new r("ig",-1,2),new r("ung",-1,1),new r("lich",-1,3),new r("isch",-1,2),new r("ik",-1,2),new r("heit",-1,3),new r("keit",-1,4)],p=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32,8],g=[117,30,5],k=[117,30,4],v=new n;this.setCurrent=function(e){v.setCurrent(e)},this.getCurrent=function(){return v.getCurrent()},this.stem=function(){var e=v.cursor;return i(),v.cursor=e,t(),v.limit_backward=e,v.cursor=v.limit,a(),v.cursor=v.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.de.stemmer,"stemmer-de"),e.de.stopWordFilter=e.generateStopWordFilter("aber alle allem allen aller alles als also am an ander andere anderem anderen anderer anderes anderm andern anderr anders auch auf aus bei bin bis bist da damit dann das dasselbe dazu daß dein deine deinem deinen deiner deines dem demselben den denn denselben der derer derselbe derselben des desselben dessen dich die dies diese dieselbe dieselben diesem diesen dieser dieses dir doch dort du durch ein eine einem einen einer eines einig einige einigem einigen einiger einiges einmal er es etwas euch euer eure eurem euren eurer eures für gegen gewesen hab habe haben hat hatte hatten hier hin hinter ich ihm ihn ihnen ihr ihre ihrem ihren ihrer ihres im in indem ins ist jede jedem jeden jeder jedes jene jenem jenen jener jenes jetzt kann kein keine keinem keinen keiner keines können könnte machen man manche manchem manchen mancher manches mein meine meinem meinen meiner meines mich mir mit muss musste nach nicht nichts noch nun nur ob oder ohne sehr sein seine seinem seinen seiner seines selbst sich sie sind so solche solchem solchen solcher solches soll sollte sondern sonst um und uns unse unsem unsen unser unses unter viel vom von vor war waren warst was weg weil weiter welche welchem welchen welcher welches wenn werde werden wie wieder will wir wird wirst wo wollen wollte während würde würden zu zum zur zwar zwischen über".split(" ")),e.Pipeline.registerFunction(e.de.stopWordFilter,"stopWordFilter-de")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.du.min.js b/v2/assets/javascripts/lunr/min/lunr.du.min.js new file mode 100644 index 000000000..49a0f3f0a --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.du.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Dutch` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");console.warn('[Lunr Languages] Please use the "nl" instead of the "du". The "nl" code is the standard code for Dutch language, and "du" will be removed in the next major versions.'),e.du=function(){this.pipeline.reset(),this.pipeline.add(e.du.trimmer,e.du.stopWordFilter,e.du.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.du.stemmer))},e.du.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.du.trimmer=e.trimmerSupport.generateTrimmer(e.du.wordCharacters),e.Pipeline.registerFunction(e.du.trimmer,"trimmer-du"),e.du.stemmer=function(){var r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){function e(){for(var e,r,i,o=C.cursor;;){if(C.bra=C.cursor,e=C.find_among(b,11))switch(C.ket=C.cursor,e){case 1:C.slice_from("a");continue;case 2:C.slice_from("e");continue;case 3:C.slice_from("i");continue;case 4:C.slice_from("o");continue;case 5:C.slice_from("u");continue;case 6:if(C.cursor>=C.limit)break;C.cursor++;continue}break}for(C.cursor=o,C.bra=o,C.eq_s(1,"y")?(C.ket=C.cursor,C.slice_from("Y")):C.cursor=o;;)if(r=C.cursor,C.in_grouping(q,97,232)){if(i=C.cursor,C.bra=i,C.eq_s(1,"i"))C.ket=C.cursor,C.in_grouping(q,97,232)&&(C.slice_from("I"),C.cursor=r);else if(C.cursor=i,C.eq_s(1,"y"))C.ket=C.cursor,C.slice_from("Y"),C.cursor=r;else if(n(r))break}else if(n(r))break}function n(e){return C.cursor=e,e>=C.limit||(C.cursor++,!1)}function o(){_=C.limit,f=_,t()||(_=C.cursor,_<3&&(_=3),t()||(f=C.cursor))}function t(){for(;!C.in_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}for(;!C.out_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}return!1}function s(){for(var e;;)if(C.bra=C.cursor,e=C.find_among(p,3))switch(C.ket=C.cursor,e){case 1:C.slice_from("y");break;case 2:C.slice_from("i");break;case 3:if(C.cursor>=C.limit)return;C.cursor++}}function u(){return _<=C.cursor}function c(){return f<=C.cursor}function a(){var e=C.limit-C.cursor;C.find_among_b(g,3)&&(C.cursor=C.limit-e,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del()))}function l(){var e;w=!1,C.ket=C.cursor,C.eq_s_b(1,"e")&&(C.bra=C.cursor,u()&&(e=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-e,C.slice_del(),w=!0,a())))}function m(){var e;u()&&(e=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-e,C.eq_s_b(3,"gem")||(C.cursor=C.limit-e,C.slice_del(),a())))}function d(){var e,r,i,n,o,t,s=C.limit-C.cursor;if(C.ket=C.cursor,e=C.find_among_b(h,5))switch(C.bra=C.cursor,e){case 1:u()&&C.slice_from("heid");break;case 2:m();break;case 3:u()&&C.out_grouping_b(z,97,232)&&C.slice_del()}if(C.cursor=C.limit-s,l(),C.cursor=C.limit-s,C.ket=C.cursor,C.eq_s_b(4,"heid")&&(C.bra=C.cursor,c()&&(r=C.limit-C.cursor,C.eq_s_b(1,"c")||(C.cursor=C.limit-r,C.slice_del(),C.ket=C.cursor,C.eq_s_b(2,"en")&&(C.bra=C.cursor,m())))),C.cursor=C.limit-s,C.ket=C.cursor,e=C.find_among_b(k,6))switch(C.bra=C.cursor,e){case 1:if(c()){if(C.slice_del(),i=C.limit-C.cursor,C.ket=C.cursor,C.eq_s_b(2,"ig")&&(C.bra=C.cursor,c()&&(n=C.limit-C.cursor,!C.eq_s_b(1,"e")))){C.cursor=C.limit-n,C.slice_del();break}C.cursor=C.limit-i,a()}break;case 2:c()&&(o=C.limit-C.cursor,C.eq_s_b(1,"e")||(C.cursor=C.limit-o,C.slice_del()));break;case 3:c()&&(C.slice_del(),l());break;case 4:c()&&C.slice_del();break;case 5:c()&&w&&C.slice_del()}C.cursor=C.limit-s,C.out_grouping_b(j,73,232)&&(t=C.limit-C.cursor,C.find_among_b(v,4)&&C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-t,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del())))}var f,_,w,b=[new r("",-1,6),new r("á",0,1),new r("ä",0,1),new r("é",0,2),new r("ë",0,2),new r("í",0,3),new r("ï",0,3),new r("ó",0,4),new r("ö",0,4),new r("ú",0,5),new r("ü",0,5)],p=[new r("",-1,3),new r("I",0,2),new r("Y",0,1)],g=[new r("dd",-1,-1),new r("kk",-1,-1),new r("tt",-1,-1)],h=[new r("ene",-1,2),new r("se",-1,3),new r("en",-1,2),new r("heden",2,1),new r("s",-1,3)],k=[new r("end",-1,1),new r("ig",-1,2),new r("ing",-1,1),new r("lijk",-1,3),new r("baar",-1,4),new r("bar",-1,5)],v=[new r("aa",-1,-1),new r("ee",-1,-1),new r("oo",-1,-1),new r("uu",-1,-1)],q=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],j=[1,0,0,17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],z=[17,67,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],C=new i;this.setCurrent=function(e){C.setCurrent(e)},this.getCurrent=function(){return C.getCurrent()},this.stem=function(){var r=C.cursor;return e(),C.cursor=r,o(),C.limit_backward=r,C.cursor=C.limit,d(),C.cursor=C.limit_backward,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.du.stemmer,"stemmer-du"),e.du.stopWordFilter=e.generateStopWordFilter(" aan al alles als altijd andere ben bij daar dan dat de der deze die dit doch doen door dus een eens en er ge geen geweest haar had heb hebben heeft hem het hier hij hoe hun iemand iets ik in is ja je kan kon kunnen maar me meer men met mij mijn moet na naar niet niets nog nu of om omdat onder ons ook op over reeds te tegen toch toen tot u uit uw van veel voor want waren was wat werd wezen wie wil worden wordt zal ze zelf zich zij zijn zo zonder zou".split(" ")),e.Pipeline.registerFunction(e.du.stopWordFilter,"stopWordFilter-du")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.es.min.js b/v2/assets/javascripts/lunr/min/lunr.es.min.js new file mode 100644 index 000000000..2989d3426 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.es.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Spanish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,s){"function"==typeof define&&define.amd?define(s):"object"==typeof exports?module.exports=s():s()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.es=function(){this.pipeline.reset(),this.pipeline.add(e.es.trimmer,e.es.stopWordFilter,e.es.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.es.stemmer))},e.es.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.es.trimmer=e.trimmerSupport.generateTrimmer(e.es.wordCharacters),e.Pipeline.registerFunction(e.es.trimmer,"trimmer-es"),e.es.stemmer=function(){var s=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){function e(){if(A.out_grouping(x,97,252)){for(;!A.in_grouping(x,97,252);){if(A.cursor>=A.limit)return!0;A.cursor++}return!1}return!0}function n(){if(A.in_grouping(x,97,252)){var s=A.cursor;if(e()){if(A.cursor=s,!A.in_grouping(x,97,252))return!0;for(;!A.out_grouping(x,97,252);){if(A.cursor>=A.limit)return!0;A.cursor++}}return!1}return!0}function i(){var s,r=A.cursor;if(n()){if(A.cursor=r,!A.out_grouping(x,97,252))return;if(s=A.cursor,e()){if(A.cursor=s,!A.in_grouping(x,97,252)||A.cursor>=A.limit)return;A.cursor++}}g=A.cursor}function a(){for(;!A.in_grouping(x,97,252);){if(A.cursor>=A.limit)return!1;A.cursor++}for(;!A.out_grouping(x,97,252);){if(A.cursor>=A.limit)return!1;A.cursor++}return!0}function t(){var e=A.cursor;g=A.limit,p=g,v=g,i(),A.cursor=e,a()&&(p=A.cursor,a()&&(v=A.cursor))}function o(){for(var e;;){if(A.bra=A.cursor,e=A.find_among(k,6))switch(A.ket=A.cursor,e){case 1:A.slice_from("a");continue;case 2:A.slice_from("e");continue;case 3:A.slice_from("i");continue;case 4:A.slice_from("o");continue;case 5:A.slice_from("u");continue;case 6:if(A.cursor>=A.limit)break;A.cursor++;continue}break}}function u(){return g<=A.cursor}function w(){return p<=A.cursor}function c(){return v<=A.cursor}function m(){var e;if(A.ket=A.cursor,A.find_among_b(y,13)&&(A.bra=A.cursor,(e=A.find_among_b(q,11))&&u()))switch(e){case 1:A.bra=A.cursor,A.slice_from("iendo");break;case 2:A.bra=A.cursor,A.slice_from("ando");break;case 3:A.bra=A.cursor,A.slice_from("ar");break;case 4:A.bra=A.cursor,A.slice_from("er");break;case 5:A.bra=A.cursor,A.slice_from("ir");break;case 6:A.slice_del();break;case 7:A.eq_s_b(1,"u")&&A.slice_del()}}function l(e,s){if(!c())return!0;A.slice_del(),A.ket=A.cursor;var r=A.find_among_b(e,s);return r&&(A.bra=A.cursor,1==r&&c()&&A.slice_del()),!1}function d(e){return!c()||(A.slice_del(),A.ket=A.cursor,A.eq_s_b(2,e)&&(A.bra=A.cursor,c()&&A.slice_del()),!1)}function b(){var e;if(A.ket=A.cursor,e=A.find_among_b(S,46)){switch(A.bra=A.cursor,e){case 1:if(!c())return!1;A.slice_del();break;case 2:if(d("ic"))return!1;break;case 3:if(!c())return!1;A.slice_from("log");break;case 4:if(!c())return!1;A.slice_from("u");break;case 5:if(!c())return!1;A.slice_from("ente");break;case 6:if(!w())return!1;A.slice_del(),A.ket=A.cursor,e=A.find_among_b(C,4),e&&(A.bra=A.cursor,c()&&(A.slice_del(),1==e&&(A.ket=A.cursor,A.eq_s_b(2,"at")&&(A.bra=A.cursor,c()&&A.slice_del()))));break;case 7:if(l(P,3))return!1;break;case 8:if(l(F,3))return!1;break;case 9:if(d("at"))return!1}return!0}return!1}function f(){var e,s;if(A.cursor>=g&&(s=A.limit_backward,A.limit_backward=g,A.ket=A.cursor,e=A.find_among_b(W,12),A.limit_backward=s,e)){if(A.bra=A.cursor,1==e){if(!A.eq_s_b(1,"u"))return!1;A.slice_del()}return!0}return!1}function _(){var e,s,r,n;if(A.cursor>=g&&(s=A.limit_backward,A.limit_backward=g,A.ket=A.cursor,e=A.find_among_b(L,96),A.limit_backward=s,e))switch(A.bra=A.cursor,e){case 1:r=A.limit-A.cursor,A.eq_s_b(1,"u")?(n=A.limit-A.cursor,A.eq_s_b(1,"g")?A.cursor=A.limit-n:A.cursor=A.limit-r):A.cursor=A.limit-r,A.bra=A.cursor;case 2:A.slice_del()}}function h(){var e,s;if(A.ket=A.cursor,e=A.find_among_b(z,8))switch(A.bra=A.cursor,e){case 1:u()&&A.slice_del();break;case 2:u()&&(A.slice_del(),A.ket=A.cursor,A.eq_s_b(1,"u")&&(A.bra=A.cursor,s=A.limit-A.cursor,A.eq_s_b(1,"g")&&(A.cursor=A.limit-s,u()&&A.slice_del())))}}var v,p,g,k=[new s("",-1,6),new s("á",0,1),new s("é",0,2),new s("í",0,3),new s("ó",0,4),new s("ú",0,5)],y=[new s("la",-1,-1),new s("sela",0,-1),new s("le",-1,-1),new s("me",-1,-1),new s("se",-1,-1),new s("lo",-1,-1),new s("selo",5,-1),new s("las",-1,-1),new s("selas",7,-1),new s("les",-1,-1),new s("los",-1,-1),new s("selos",10,-1),new s("nos",-1,-1)],q=[new s("ando",-1,6),new s("iendo",-1,6),new s("yendo",-1,7),new s("ándo",-1,2),new s("iéndo",-1,1),new s("ar",-1,6),new s("er",-1,6),new s("ir",-1,6),new s("ár",-1,3),new s("ér",-1,4),new s("ír",-1,5)],C=[new s("ic",-1,-1),new s("ad",-1,-1),new s("os",-1,-1),new s("iv",-1,1)],P=[new s("able",-1,1),new s("ible",-1,1),new s("ante",-1,1)],F=[new s("ic",-1,1),new s("abil",-1,1),new s("iv",-1,1)],S=[new s("ica",-1,1),new s("ancia",-1,2),new s("encia",-1,5),new s("adora",-1,2),new s("osa",-1,1),new s("ista",-1,1),new s("iva",-1,9),new s("anza",-1,1),new s("logía",-1,3),new s("idad",-1,8),new s("able",-1,1),new s("ible",-1,1),new s("ante",-1,2),new s("mente",-1,7),new s("amente",13,6),new s("ación",-1,2),new s("ución",-1,4),new s("ico",-1,1),new s("ismo",-1,1),new s("oso",-1,1),new s("amiento",-1,1),new s("imiento",-1,1),new s("ivo",-1,9),new s("ador",-1,2),new s("icas",-1,1),new s("ancias",-1,2),new s("encias",-1,5),new s("adoras",-1,2),new s("osas",-1,1),new s("istas",-1,1),new s("ivas",-1,9),new s("anzas",-1,1),new s("logías",-1,3),new s("idades",-1,8),new s("ables",-1,1),new s("ibles",-1,1),new s("aciones",-1,2),new s("uciones",-1,4),new s("adores",-1,2),new s("antes",-1,2),new s("icos",-1,1),new s("ismos",-1,1),new s("osos",-1,1),new s("amientos",-1,1),new s("imientos",-1,1),new s("ivos",-1,9)],W=[new s("ya",-1,1),new s("ye",-1,1),new s("yan",-1,1),new s("yen",-1,1),new s("yeron",-1,1),new s("yendo",-1,1),new s("yo",-1,1),new s("yas",-1,1),new s("yes",-1,1),new s("yais",-1,1),new s("yamos",-1,1),new s("yó",-1,1)],L=[new s("aba",-1,2),new s("ada",-1,2),new s("ida",-1,2),new s("ara",-1,2),new s("iera",-1,2),new s("ía",-1,2),new s("aría",5,2),new s("ería",5,2),new s("iría",5,2),new s("ad",-1,2),new s("ed",-1,2),new s("id",-1,2),new s("ase",-1,2),new s("iese",-1,2),new s("aste",-1,2),new s("iste",-1,2),new s("an",-1,2),new s("aban",16,2),new s("aran",16,2),new s("ieran",16,2),new s("ían",16,2),new s("arían",20,2),new s("erían",20,2),new s("irían",20,2),new s("en",-1,1),new s("asen",24,2),new s("iesen",24,2),new s("aron",-1,2),new s("ieron",-1,2),new s("arán",-1,2),new s("erán",-1,2),new s("irán",-1,2),new s("ado",-1,2),new s("ido",-1,2),new s("ando",-1,2),new s("iendo",-1,2),new s("ar",-1,2),new s("er",-1,2),new s("ir",-1,2),new s("as",-1,2),new s("abas",39,2),new s("adas",39,2),new s("idas",39,2),new s("aras",39,2),new s("ieras",39,2),new s("ías",39,2),new s("arías",45,2),new s("erías",45,2),new s("irías",45,2),new s("es",-1,1),new s("ases",49,2),new s("ieses",49,2),new s("abais",-1,2),new s("arais",-1,2),new s("ierais",-1,2),new s("íais",-1,2),new s("aríais",55,2),new s("eríais",55,2),new s("iríais",55,2),new s("aseis",-1,2),new s("ieseis",-1,2),new s("asteis",-1,2),new s("isteis",-1,2),new s("áis",-1,2),new s("éis",-1,1),new s("aréis",64,2),new s("eréis",64,2),new s("iréis",64,2),new s("ados",-1,2),new s("idos",-1,2),new s("amos",-1,2),new s("ábamos",70,2),new s("áramos",70,2),new s("iéramos",70,2),new s("íamos",70,2),new s("aríamos",74,2),new s("eríamos",74,2),new s("iríamos",74,2),new s("emos",-1,1),new s("aremos",78,2),new s("eremos",78,2),new s("iremos",78,2),new s("ásemos",78,2),new s("iésemos",78,2),new s("imos",-1,2),new s("arás",-1,2),new s("erás",-1,2),new s("irás",-1,2),new s("ís",-1,2),new s("ará",-1,2),new s("erá",-1,2),new s("irá",-1,2),new s("aré",-1,2),new s("eré",-1,2),new s("iré",-1,2),new s("ió",-1,2)],z=[new s("a",-1,1),new s("e",-1,2),new s("o",-1,1),new s("os",-1,1),new s("á",-1,1),new s("é",-1,2),new s("í",-1,1),new s("ó",-1,1)],x=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,1,17,4,10],A=new r;this.setCurrent=function(e){A.setCurrent(e)},this.getCurrent=function(){return A.getCurrent()},this.stem=function(){var e=A.cursor;return t(),A.limit_backward=e,A.cursor=A.limit,m(),A.cursor=A.limit,b()||(A.cursor=A.limit,f()||(A.cursor=A.limit,_())),A.cursor=A.limit,h(),A.cursor=A.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.es.stemmer,"stemmer-es"),e.es.stopWordFilter=e.generateStopWordFilter("a al algo algunas algunos ante antes como con contra cual cuando de del desde donde durante e el ella ellas ellos en entre era erais eran eras eres es esa esas ese eso esos esta estaba estabais estaban estabas estad estada estadas estado estados estamos estando estar estaremos estará estarán estarás estaré estaréis estaría estaríais estaríamos estarían estarías estas este estemos esto estos estoy estuve estuviera estuvierais estuvieran estuvieras estuvieron estuviese estuvieseis estuviesen estuvieses estuvimos estuviste estuvisteis estuviéramos estuviésemos estuvo está estábamos estáis están estás esté estéis estén estés fue fuera fuerais fueran fueras fueron fuese fueseis fuesen fueses fui fuimos fuiste fuisteis fuéramos fuésemos ha habida habidas habido habidos habiendo habremos habrá habrán habrás habré habréis habría habríais habríamos habrían habrías habéis había habíais habíamos habían habías han has hasta hay haya hayamos hayan hayas hayáis he hemos hube hubiera hubierais hubieran hubieras hubieron hubiese hubieseis hubiesen hubieses hubimos hubiste hubisteis hubiéramos hubiésemos hubo la las le les lo los me mi mis mucho muchos muy más mí mía mías mío míos nada ni no nos nosotras nosotros nuestra nuestras nuestro nuestros o os otra otras otro otros para pero poco por porque que quien quienes qué se sea seamos sean seas seremos será serán serás seré seréis sería seríais seríamos serían serías seáis sido siendo sin sobre sois somos son soy su sus suya suyas suyo suyos sí también tanto te tendremos tendrá tendrán tendrás tendré tendréis tendría tendríais tendríamos tendrían tendrías tened tenemos tenga tengamos tengan tengas tengo tengáis tenida tenidas tenido tenidos teniendo tenéis tenía teníais teníamos tenían tenías ti tiene tienen tienes todo todos tu tus tuve tuviera tuvierais tuvieran tuvieras tuvieron tuviese tuvieseis tuviesen tuvieses tuvimos tuviste tuvisteis tuviéramos tuviésemos tuvo tuya tuyas tuyo tuyos tú un una uno unos vosotras vosotros vuestra vuestras vuestro vuestros y ya yo él éramos".split(" ")),e.Pipeline.registerFunction(e.es.stopWordFilter,"stopWordFilter-es")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.fi.min.js b/v2/assets/javascripts/lunr/min/lunr.fi.min.js new file mode 100644 index 000000000..29f5dfcea --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.fi.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Finnish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(i,e){"function"==typeof define&&define.amd?define(e):"object"==typeof exports?module.exports=e():e()(i.lunr)}(this,function(){return function(i){if(void 0===i)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===i.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");i.fi=function(){this.pipeline.reset(),this.pipeline.add(i.fi.trimmer,i.fi.stopWordFilter,i.fi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(i.fi.stemmer))},i.fi.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",i.fi.trimmer=i.trimmerSupport.generateTrimmer(i.fi.wordCharacters),i.Pipeline.registerFunction(i.fi.trimmer,"trimmer-fi"),i.fi.stemmer=function(){var e=i.stemmerSupport.Among,r=i.stemmerSupport.SnowballProgram,n=new function(){function i(){f=A.limit,d=f,n()||(f=A.cursor,n()||(d=A.cursor))}function n(){for(var i;;){if(i=A.cursor,A.in_grouping(W,97,246))break;if(A.cursor=i,i>=A.limit)return!0;A.cursor++}for(A.cursor=i;!A.out_grouping(W,97,246);){if(A.cursor>=A.limit)return!0;A.cursor++}return!1}function t(){return d<=A.cursor}function s(){var i,e;if(A.cursor>=f)if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,i=A.find_among_b(h,10)){switch(A.bra=A.cursor,A.limit_backward=e,i){case 1:if(!A.in_grouping_b(x,97,246))return;break;case 2:if(!t())return}A.slice_del()}else A.limit_backward=e}function o(){var i,e,r;if(A.cursor>=f)if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,i=A.find_among_b(v,9))switch(A.bra=A.cursor,A.limit_backward=e,i){case 1:r=A.limit-A.cursor,A.eq_s_b(1,"k")||(A.cursor=A.limit-r,A.slice_del());break;case 2:A.slice_del(),A.ket=A.cursor,A.eq_s_b(3,"kse")&&(A.bra=A.cursor,A.slice_from("ksi"));break;case 3:A.slice_del();break;case 4:A.find_among_b(p,6)&&A.slice_del();break;case 5:A.find_among_b(g,6)&&A.slice_del();break;case 6:A.find_among_b(j,2)&&A.slice_del()}else A.limit_backward=e}function l(){return A.find_among_b(q,7)}function a(){return A.eq_s_b(1,"i")&&A.in_grouping_b(L,97,246)}function u(){var i,e,r;if(A.cursor>=f)if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,i=A.find_among_b(C,30)){switch(A.bra=A.cursor,A.limit_backward=e,i){case 1:if(!A.eq_s_b(1,"a"))return;break;case 2:case 9:if(!A.eq_s_b(1,"e"))return;break;case 3:if(!A.eq_s_b(1,"i"))return;break;case 4:if(!A.eq_s_b(1,"o"))return;break;case 5:if(!A.eq_s_b(1,"ä"))return;break;case 6:if(!A.eq_s_b(1,"ö"))return;break;case 7:if(r=A.limit-A.cursor,!l()&&(A.cursor=A.limit-r,!A.eq_s_b(2,"ie"))){A.cursor=A.limit-r;break}if(A.cursor=A.limit-r,A.cursor<=A.limit_backward){A.cursor=A.limit-r;break}A.cursor--,A.bra=A.cursor;break;case 8:if(!A.in_grouping_b(W,97,246)||!A.out_grouping_b(W,97,246))return}A.slice_del(),k=!0}else A.limit_backward=e}function c(){var i,e,r;if(A.cursor>=d)if(e=A.limit_backward,A.limit_backward=d,A.ket=A.cursor,i=A.find_among_b(P,14)){if(A.bra=A.cursor,A.limit_backward=e,1==i){if(r=A.limit-A.cursor,A.eq_s_b(2,"po"))return;A.cursor=A.limit-r}A.slice_del()}else A.limit_backward=e}function m(){var i;A.cursor>=f&&(i=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,A.find_among_b(F,2)?(A.bra=A.cursor,A.limit_backward=i,A.slice_del()):A.limit_backward=i)}function w(){var i,e,r,n,t,s;if(A.cursor>=f){if(e=A.limit_backward,A.limit_backward=f,A.ket=A.cursor,A.eq_s_b(1,"t")&&(A.bra=A.cursor,r=A.limit-A.cursor,A.in_grouping_b(W,97,246)&&(A.cursor=A.limit-r,A.slice_del(),A.limit_backward=e,n=A.limit-A.cursor,A.cursor>=d&&(A.cursor=d,t=A.limit_backward,A.limit_backward=A.cursor,A.cursor=A.limit-n,A.ket=A.cursor,i=A.find_among_b(S,2))))){if(A.bra=A.cursor,A.limit_backward=t,1==i){if(s=A.limit-A.cursor,A.eq_s_b(2,"po"))return;A.cursor=A.limit-s}return void A.slice_del()}A.limit_backward=e}}function _(){var i,e,r,n;if(A.cursor>=f){for(i=A.limit_backward,A.limit_backward=f,e=A.limit-A.cursor,l()&&(A.cursor=A.limit-e,A.ket=A.cursor,A.cursor>A.limit_backward&&(A.cursor--,A.bra=A.cursor,A.slice_del())),A.cursor=A.limit-e,A.ket=A.cursor,A.in_grouping_b(y,97,228)&&(A.bra=A.cursor,A.out_grouping_b(W,97,246)&&A.slice_del()),A.cursor=A.limit-e,A.ket=A.cursor,A.eq_s_b(1,"j")&&(A.bra=A.cursor,r=A.limit-A.cursor,A.eq_s_b(1,"o")?A.slice_del():(A.cursor=A.limit-r,A.eq_s_b(1,"u")&&A.slice_del())),A.cursor=A.limit-e,A.ket=A.cursor,A.eq_s_b(1,"o")&&(A.bra=A.cursor,A.eq_s_b(1,"j")&&A.slice_del()),A.cursor=A.limit-e,A.limit_backward=i;;){if(n=A.limit-A.cursor,A.out_grouping_b(W,97,246)){A.cursor=A.limit-n;break}if(A.cursor=A.limit-n,A.cursor<=A.limit_backward)return;A.cursor--}A.ket=A.cursor,A.cursor>A.limit_backward&&(A.cursor--,A.bra=A.cursor,b=A.slice_to(),A.eq_v_b(b)&&A.slice_del())}}var k,b,d,f,h=[new e("pa",-1,1),new e("sti",-1,2),new e("kaan",-1,1),new e("han",-1,1),new e("kin",-1,1),new e("hän",-1,1),new e("kään",-1,1),new e("ko",-1,1),new e("pä",-1,1),new e("kö",-1,1)],p=[new e("lla",-1,-1),new e("na",-1,-1),new e("ssa",-1,-1),new e("ta",-1,-1),new e("lta",3,-1),new e("sta",3,-1)],g=[new e("llä",-1,-1),new e("nä",-1,-1),new e("ssä",-1,-1),new e("tä",-1,-1),new e("ltä",3,-1),new e("stä",3,-1)],j=[new e("lle",-1,-1),new e("ine",-1,-1)],v=[new e("nsa",-1,3),new e("mme",-1,3),new e("nne",-1,3),new e("ni",-1,2),new e("si",-1,1),new e("an",-1,4),new e("en",-1,6),new e("än",-1,5),new e("nsä",-1,3)],q=[new e("aa",-1,-1),new e("ee",-1,-1),new e("ii",-1,-1),new e("oo",-1,-1),new e("uu",-1,-1),new e("ää",-1,-1),new e("öö",-1,-1)],C=[new e("a",-1,8),new e("lla",0,-1),new e("na",0,-1),new e("ssa",0,-1),new e("ta",0,-1),new e("lta",4,-1),new e("sta",4,-1),new e("tta",4,9),new e("lle",-1,-1),new e("ine",-1,-1),new e("ksi",-1,-1),new e("n",-1,7),new e("han",11,1),new e("den",11,-1,a),new e("seen",11,-1,l),new e("hen",11,2),new e("tten",11,-1,a),new e("hin",11,3),new e("siin",11,-1,a),new e("hon",11,4),new e("hän",11,5),new e("hön",11,6),new e("ä",-1,8),new e("llä",22,-1),new e("nä",22,-1),new e("ssä",22,-1),new e("tä",22,-1),new e("ltä",26,-1),new e("stä",26,-1),new e("ttä",26,9)],P=[new e("eja",-1,-1),new e("mma",-1,1),new e("imma",1,-1),new e("mpa",-1,1),new e("impa",3,-1),new e("mmi",-1,1),new e("immi",5,-1),new e("mpi",-1,1),new e("impi",7,-1),new e("ejä",-1,-1),new e("mmä",-1,1),new e("immä",10,-1),new e("mpä",-1,1),new e("impä",12,-1)],F=[new e("i",-1,-1),new e("j",-1,-1)],S=[new e("mma",-1,1),new e("imma",0,-1)],y=[17,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8],W=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],L=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],x=[17,97,24,1,0,0,0,0,0,0,0,0,0,0,0,0,8,0,32],A=new r;this.setCurrent=function(i){A.setCurrent(i)},this.getCurrent=function(){return A.getCurrent()},this.stem=function(){var e=A.cursor;return i(),k=!1,A.limit_backward=e,A.cursor=A.limit,s(),A.cursor=A.limit,o(),A.cursor=A.limit,u(),A.cursor=A.limit,c(),A.cursor=A.limit,k?(m(),A.cursor=A.limit):(A.cursor=A.limit,w(),A.cursor=A.limit),_(),!0}};return function(i){return"function"==typeof i.update?i.update(function(i){return n.setCurrent(i),n.stem(),n.getCurrent()}):(n.setCurrent(i),n.stem(),n.getCurrent())}}(),i.Pipeline.registerFunction(i.fi.stemmer,"stemmer-fi"),i.fi.stopWordFilter=i.generateStopWordFilter("ei eivät emme en et ette että he heidän heidät heihin heille heillä heiltä heissä heistä heitä hän häneen hänelle hänellä häneltä hänen hänessä hänestä hänet häntä itse ja johon joiden joihin joiksi joilla joille joilta joina joissa joista joita joka joksi jolla jolle jolta jona jonka jos jossa josta jota jotka kanssa keiden keihin keiksi keille keillä keiltä keinä keissä keistä keitä keneen keneksi kenelle kenellä keneltä kenen kenenä kenessä kenestä kenet ketkä ketkä ketä koska kuin kuka kun me meidän meidät meihin meille meillä meiltä meissä meistä meitä mihin miksi mikä mille millä miltä minkä minkä minua minulla minulle minulta minun minussa minusta minut minuun minä minä missä mistä mitkä mitä mukaan mutta ne niiden niihin niiksi niille niillä niiltä niin niin niinä niissä niistä niitä noiden noihin noiksi noilla noille noilta noin noina noissa noista noita nuo nyt näiden näihin näiksi näille näillä näiltä näinä näissä näistä näitä nämä ole olemme olen olet olette oli olimme olin olisi olisimme olisin olisit olisitte olisivat olit olitte olivat olla olleet ollut on ovat poikki se sekä sen siihen siinä siitä siksi sille sillä sillä siltä sinua sinulla sinulle sinulta sinun sinussa sinusta sinut sinuun sinä sinä sitä tai te teidän teidät teihin teille teillä teiltä teissä teistä teitä tuo tuohon tuoksi tuolla tuolle tuolta tuon tuona tuossa tuosta tuota tähän täksi tälle tällä tältä tämä tämän tänä tässä tästä tätä vaan vai vaikka yli".split(" ")),i.Pipeline.registerFunction(i.fi.stopWordFilter,"stopWordFilter-fi")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.fr.min.js b/v2/assets/javascripts/lunr/min/lunr.fr.min.js new file mode 100644 index 000000000..68cd0094a --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.fr.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `French` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.fr=function(){this.pipeline.reset(),this.pipeline.add(e.fr.trimmer,e.fr.stopWordFilter,e.fr.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.fr.stemmer))},e.fr.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.fr.trimmer=e.trimmerSupport.generateTrimmer(e.fr.wordCharacters),e.Pipeline.registerFunction(e.fr.trimmer,"trimmer-fr"),e.fr.stemmer=function(){var r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,i=new function(){function e(e,r,s){return!(!W.eq_s(1,e)||(W.ket=W.cursor,!W.in_grouping(F,97,251)))&&(W.slice_from(r),W.cursor=s,!0)}function i(e,r,s){return!!W.eq_s(1,e)&&(W.ket=W.cursor,W.slice_from(r),W.cursor=s,!0)}function n(){for(var r,s;;){if(r=W.cursor,W.in_grouping(F,97,251)){if(W.bra=W.cursor,s=W.cursor,e("u","U",r))continue;if(W.cursor=s,e("i","I",r))continue;if(W.cursor=s,i("y","Y",r))continue}if(W.cursor=r,W.bra=r,!e("y","Y",r)){if(W.cursor=r,W.eq_s(1,"q")&&(W.bra=W.cursor,i("u","U",r)))continue;if(W.cursor=r,r>=W.limit)return;W.cursor++}}}function t(){for(;!W.in_grouping(F,97,251);){if(W.cursor>=W.limit)return!0;W.cursor++}for(;!W.out_grouping(F,97,251);){if(W.cursor>=W.limit)return!0;W.cursor++}return!1}function u(){var e=W.cursor;if(q=W.limit,g=q,p=q,W.in_grouping(F,97,251)&&W.in_grouping(F,97,251)&&W.cursor=W.limit){W.cursor=q;break}W.cursor++}while(!W.in_grouping(F,97,251))}q=W.cursor,W.cursor=e,t()||(g=W.cursor,t()||(p=W.cursor))}function o(){for(var e,r;;){if(r=W.cursor,W.bra=r,!(e=W.find_among(h,4)))break;switch(W.ket=W.cursor,e){case 1:W.slice_from("i");break;case 2:W.slice_from("u");break;case 3:W.slice_from("y");break;case 4:if(W.cursor>=W.limit)return;W.cursor++}}}function c(){return q<=W.cursor}function a(){return g<=W.cursor}function l(){return p<=W.cursor}function w(){var e,r;if(W.ket=W.cursor,e=W.find_among_b(C,43)){switch(W.bra=W.cursor,e){case 1:if(!l())return!1;W.slice_del();break;case 2:if(!l())return!1;W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"ic")&&(W.bra=W.cursor,l()?W.slice_del():W.slice_from("iqU"));break;case 3:if(!l())return!1;W.slice_from("log");break;case 4:if(!l())return!1;W.slice_from("u");break;case 5:if(!l())return!1;W.slice_from("ent");break;case 6:if(!c())return!1;if(W.slice_del(),W.ket=W.cursor,e=W.find_among_b(z,6))switch(W.bra=W.cursor,e){case 1:l()&&(W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"at")&&(W.bra=W.cursor,l()&&W.slice_del()));break;case 2:l()?W.slice_del():a()&&W.slice_from("eux");break;case 3:l()&&W.slice_del();break;case 4:c()&&W.slice_from("i")}break;case 7:if(!l())return!1;if(W.slice_del(),W.ket=W.cursor,e=W.find_among_b(y,3))switch(W.bra=W.cursor,e){case 1:l()?W.slice_del():W.slice_from("abl");break;case 2:l()?W.slice_del():W.slice_from("iqU");break;case 3:l()&&W.slice_del()}break;case 8:if(!l())return!1;if(W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"at")&&(W.bra=W.cursor,l()&&(W.slice_del(),W.ket=W.cursor,W.eq_s_b(2,"ic")))){W.bra=W.cursor,l()?W.slice_del():W.slice_from("iqU");break}break;case 9:W.slice_from("eau");break;case 10:if(!a())return!1;W.slice_from("al");break;case 11:if(l())W.slice_del();else{if(!a())return!1;W.slice_from("eux")}break;case 12:if(!a()||!W.out_grouping_b(F,97,251))return!1;W.slice_del();break;case 13:return c()&&W.slice_from("ant"),!1;case 14:return c()&&W.slice_from("ent"),!1;case 15:return r=W.limit-W.cursor,W.in_grouping_b(F,97,251)&&c()&&(W.cursor=W.limit-r,W.slice_del()),!1}return!0}return!1}function f(){var e,r;if(W.cursor=q){if(s=W.limit_backward,W.limit_backward=q,W.ket=W.cursor,e=W.find_among_b(P,7))switch(W.bra=W.cursor,e){case 1:if(l()){if(i=W.limit-W.cursor,!W.eq_s_b(1,"s")&&(W.cursor=W.limit-i,!W.eq_s_b(1,"t")))break;W.slice_del()}break;case 2:W.slice_from("i");break;case 3:W.slice_del();break;case 4:W.eq_s_b(2,"gu")&&W.slice_del()}W.limit_backward=s}}function b(){var e=W.limit-W.cursor;W.find_among_b(U,5)&&(W.cursor=W.limit-e,W.ket=W.cursor,W.cursor>W.limit_backward&&(W.cursor--,W.bra=W.cursor,W.slice_del()))}function d(){for(var e,r=1;W.out_grouping_b(F,97,251);)r--;if(r<=0){if(W.ket=W.cursor,e=W.limit-W.cursor,!W.eq_s_b(1,"é")&&(W.cursor=W.limit-e,!W.eq_s_b(1,"è")))return;W.bra=W.cursor,W.slice_from("e")}}function k(){if(!w()&&(W.cursor=W.limit,!f()&&(W.cursor=W.limit,!m())))return W.cursor=W.limit,void _();W.cursor=W.limit,W.ket=W.cursor,W.eq_s_b(1,"Y")?(W.bra=W.cursor,W.slice_from("i")):(W.cursor=W.limit,W.eq_s_b(1,"ç")&&(W.bra=W.cursor,W.slice_from("c")))}var p,g,q,v=[new r("col",-1,-1),new r("par",-1,-1),new r("tap",-1,-1)],h=[new r("",-1,4),new r("I",0,1),new r("U",0,2),new r("Y",0,3)],z=[new r("iqU",-1,3),new r("abl",-1,3),new r("Ièr",-1,4),new r("ièr",-1,4),new r("eus",-1,2),new r("iv",-1,1)],y=[new r("ic",-1,2),new r("abil",-1,1),new r("iv",-1,3)],C=[new r("iqUe",-1,1),new r("atrice",-1,2),new r("ance",-1,1),new r("ence",-1,5),new r("logie",-1,3),new r("able",-1,1),new r("isme",-1,1),new r("euse",-1,11),new r("iste",-1,1),new r("ive",-1,8),new r("if",-1,8),new r("usion",-1,4),new r("ation",-1,2),new r("ution",-1,4),new r("ateur",-1,2),new r("iqUes",-1,1),new r("atrices",-1,2),new r("ances",-1,1),new r("ences",-1,5),new r("logies",-1,3),new r("ables",-1,1),new r("ismes",-1,1),new r("euses",-1,11),new r("istes",-1,1),new r("ives",-1,8),new r("ifs",-1,8),new r("usions",-1,4),new r("ations",-1,2),new r("utions",-1,4),new r("ateurs",-1,2),new r("ments",-1,15),new r("ements",30,6),new r("issements",31,12),new r("ités",-1,7),new r("ment",-1,15),new r("ement",34,6),new r("issement",35,12),new r("amment",34,13),new r("emment",34,14),new r("aux",-1,10),new r("eaux",39,9),new r("eux",-1,1),new r("ité",-1,7)],x=[new r("ira",-1,1),new r("ie",-1,1),new r("isse",-1,1),new r("issante",-1,1),new r("i",-1,1),new r("irai",4,1),new r("ir",-1,1),new r("iras",-1,1),new r("ies",-1,1),new r("îmes",-1,1),new r("isses",-1,1),new r("issantes",-1,1),new r("îtes",-1,1),new r("is",-1,1),new r("irais",13,1),new r("issais",13,1),new r("irions",-1,1),new r("issions",-1,1),new r("irons",-1,1),new r("issons",-1,1),new r("issants",-1,1),new r("it",-1,1),new r("irait",21,1),new r("issait",21,1),new r("issant",-1,1),new r("iraIent",-1,1),new r("issaIent",-1,1),new r("irent",-1,1),new r("issent",-1,1),new r("iront",-1,1),new r("ît",-1,1),new r("iriez",-1,1),new r("issiez",-1,1),new r("irez",-1,1),new r("issez",-1,1)],I=[new r("a",-1,3),new r("era",0,2),new r("asse",-1,3),new r("ante",-1,3),new r("ée",-1,2),new r("ai",-1,3),new r("erai",5,2),new r("er",-1,2),new r("as",-1,3),new r("eras",8,2),new r("âmes",-1,3),new r("asses",-1,3),new r("antes",-1,3),new r("âtes",-1,3),new r("ées",-1,2),new r("ais",-1,3),new r("erais",15,2),new r("ions",-1,1),new r("erions",17,2),new r("assions",17,3),new r("erons",-1,2),new r("ants",-1,3),new r("és",-1,2),new r("ait",-1,3),new r("erait",23,2),new r("ant",-1,3),new r("aIent",-1,3),new r("eraIent",26,2),new r("èrent",-1,2),new r("assent",-1,3),new r("eront",-1,2),new r("ât",-1,3),new r("ez",-1,2),new r("iez",32,2),new r("eriez",33,2),new r("assiez",33,3),new r("erez",32,2),new r("é",-1,2)],P=[new r("e",-1,3),new r("Ière",0,2),new r("ière",0,2),new r("ion",-1,1),new r("Ier",-1,2),new r("ier",-1,2),new r("ë",-1,4)],U=[new r("ell",-1,-1),new r("eill",-1,-1),new r("enn",-1,-1),new r("onn",-1,-1),new r("ett",-1,-1)],F=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,128,130,103,8,5],S=[1,65,20,0,0,0,0,0,0,0,0,0,0,0,0,0,128],W=new s;this.setCurrent=function(e){W.setCurrent(e)},this.getCurrent=function(){return W.getCurrent()},this.stem=function(){var e=W.cursor;return n(),W.cursor=e,u(),W.limit_backward=e,W.cursor=W.limit,k(),W.cursor=W.limit,b(),W.cursor=W.limit,d(),W.cursor=W.limit_backward,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.fr.stemmer,"stemmer-fr"),e.fr.stopWordFilter=e.generateStopWordFilter("ai aie aient aies ait as au aura aurai auraient aurais aurait auras aurez auriez aurions aurons auront aux avaient avais avait avec avez aviez avions avons ayant ayez ayons c ce ceci celà ces cet cette d dans de des du elle en es est et eu eue eues eurent eus eusse eussent eusses eussiez eussions eut eux eûmes eût eûtes furent fus fusse fussent fusses fussiez fussions fut fûmes fût fûtes ici il ils j je l la le les leur leurs lui m ma mais me mes moi mon même n ne nos notre nous on ont ou par pas pour qu que quel quelle quelles quels qui s sa sans se sera serai seraient serais serait seras serez seriez serions serons seront ses soi soient sois soit sommes son sont soyez soyons suis sur t ta te tes toi ton tu un une vos votre vous y à étaient étais était étant étiez étions été étée étées étés êtes".split(" ")),e.Pipeline.registerFunction(e.fr.stopWordFilter,"stopWordFilter-fr")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.hu.min.js b/v2/assets/javascripts/lunr/min/lunr.hu.min.js new file mode 100644 index 000000000..ed9d909f7 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.hu.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Hungarian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hu=function(){this.pipeline.reset(),this.pipeline.add(e.hu.trimmer,e.hu.stopWordFilter,e.hu.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hu.stemmer))},e.hu.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.hu.trimmer=e.trimmerSupport.generateTrimmer(e.hu.wordCharacters),e.Pipeline.registerFunction(e.hu.trimmer,"trimmer-hu"),e.hu.stemmer=function(){var n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,n=L.cursor;if(d=L.limit,L.in_grouping(W,97,252))for(;;){if(e=L.cursor,L.out_grouping(W,97,252))return L.cursor=e,L.find_among(g,8)||(L.cursor=e,e=L.limit)return void(d=e);L.cursor++}if(L.cursor=n,L.out_grouping(W,97,252)){for(;!L.in_grouping(W,97,252);){if(L.cursor>=L.limit)return;L.cursor++}d=L.cursor}}function i(){return d<=L.cursor}function a(){var e;if(L.ket=L.cursor,(e=L.find_among_b(h,2))&&(L.bra=L.cursor,i()))switch(e){case 1:L.slice_from("a");break;case 2:L.slice_from("e")}}function t(){var e=L.limit-L.cursor;return!!L.find_among_b(p,23)&&(L.cursor=L.limit-e,!0)}function s(){if(L.cursor>L.limit_backward){L.cursor--,L.ket=L.cursor;var e=L.cursor-1;L.limit_backward<=e&&e<=L.limit&&(L.cursor=e,L.bra=e,L.slice_del())}}function c(){var e;if(L.ket=L.cursor,(e=L.find_among_b(_,2))&&(L.bra=L.cursor,i())){if((1==e||2==e)&&!t())return;L.slice_del(),s()}}function o(){L.ket=L.cursor,L.find_among_b(v,44)&&(L.bra=L.cursor,i()&&(L.slice_del(),a()))}function w(){var e;if(L.ket=L.cursor,(e=L.find_among_b(z,3))&&(L.bra=L.cursor,i()))switch(e){case 1:L.slice_from("e");break;case 2:case 3:L.slice_from("a")}}function l(){var e;if(L.ket=L.cursor,(e=L.find_among_b(y,6))&&(L.bra=L.cursor,i()))switch(e){case 1:case 2:L.slice_del();break;case 3:L.slice_from("a");break;case 4:L.slice_from("e")}}function u(){var e;if(L.ket=L.cursor,(e=L.find_among_b(j,2))&&(L.bra=L.cursor,i())){if((1==e||2==e)&&!t())return;L.slice_del(),s()}}function m(){var e;if(L.ket=L.cursor,(e=L.find_among_b(C,7))&&(L.bra=L.cursor,i()))switch(e){case 1:L.slice_from("a");break;case 2:L.slice_from("e");break;case 3:case 4:case 5:case 6:case 7:L.slice_del()}}function k(){var e;if(L.ket=L.cursor,(e=L.find_among_b(P,12))&&(L.bra=L.cursor,i()))switch(e){case 1:case 4:case 7:case 9:L.slice_del();break;case 2:case 5:case 8:L.slice_from("e");break;case 3:case 6:L.slice_from("a")}}function f(){var e;if(L.ket=L.cursor,(e=L.find_among_b(F,31))&&(L.bra=L.cursor,i()))switch(e){case 1:case 4:case 7:case 8:case 9:case 12:case 13:case 16:case 17:case 18:L.slice_del();break;case 2:case 5:case 10:case 14:case 19:L.slice_from("a");break;case 3:case 6:case 11:case 15:case 20:L.slice_from("e")}}function b(){var e;if(L.ket=L.cursor,(e=L.find_among_b(S,42))&&(L.bra=L.cursor,i()))switch(e){case 1:case 4:case 5:case 6:case 9:case 10:case 11:case 14:case 15:case 16:case 17:case 20:case 21:case 24:case 25:case 26:case 29:L.slice_del();break;case 2:case 7:case 12:case 18:case 22:case 27:L.slice_from("a");break;case 3:case 8:case 13:case 19:case 23:case 28:L.slice_from("e")}}var d,g=[new n("cs",-1,-1),new n("dzs",-1,-1),new n("gy",-1,-1),new n("ly",-1,-1),new n("ny",-1,-1),new n("sz",-1,-1),new n("ty",-1,-1),new n("zs",-1,-1)],h=[new n("á",-1,1),new n("é",-1,2)],p=[new n("bb",-1,-1),new n("cc",-1,-1),new n("dd",-1,-1),new n("ff",-1,-1),new n("gg",-1,-1),new n("jj",-1,-1),new n("kk",-1,-1),new n("ll",-1,-1),new n("mm",-1,-1),new n("nn",-1,-1),new n("pp",-1,-1),new n("rr",-1,-1),new n("ccs",-1,-1),new n("ss",-1,-1),new n("zzs",-1,-1),new n("tt",-1,-1),new n("vv",-1,-1),new n("ggy",-1,-1),new n("lly",-1,-1),new n("nny",-1,-1),new n("tty",-1,-1),new n("ssz",-1,-1),new n("zz",-1,-1)],_=[new n("al",-1,1),new n("el",-1,2)],v=[new n("ba",-1,-1),new n("ra",-1,-1),new n("be",-1,-1),new n("re",-1,-1),new n("ig",-1,-1),new n("nak",-1,-1),new n("nek",-1,-1),new n("val",-1,-1),new n("vel",-1,-1),new n("ul",-1,-1),new n("nál",-1,-1),new n("nél",-1,-1),new n("ból",-1,-1),new n("ról",-1,-1),new n("tól",-1,-1),new n("bõl",-1,-1),new n("rõl",-1,-1),new n("tõl",-1,-1),new n("ül",-1,-1),new n("n",-1,-1),new n("an",19,-1),new n("ban",20,-1),new n("en",19,-1),new n("ben",22,-1),new n("képpen",22,-1),new n("on",19,-1),new n("ön",19,-1),new n("képp",-1,-1),new n("kor",-1,-1),new n("t",-1,-1),new n("at",29,-1),new n("et",29,-1),new n("ként",29,-1),new n("anként",32,-1),new n("enként",32,-1),new n("onként",32,-1),new n("ot",29,-1),new n("ért",29,-1),new n("öt",29,-1),new n("hez",-1,-1),new n("hoz",-1,-1),new n("höz",-1,-1),new n("vá",-1,-1),new n("vé",-1,-1)],z=[new n("án",-1,2),new n("én",-1,1),new n("ánként",-1,3)],y=[new n("stul",-1,2),new n("astul",0,1),new n("ástul",0,3),new n("stül",-1,2),new n("estül",3,1),new n("éstül",3,4)],j=[new n("á",-1,1),new n("é",-1,2)],C=[new n("k",-1,7),new n("ak",0,4),new n("ek",0,6),new n("ok",0,5),new n("ák",0,1),new n("ék",0,2),new n("ök",0,3)],P=[new n("éi",-1,7),new n("áéi",0,6),new n("ééi",0,5),new n("é",-1,9),new n("ké",3,4),new n("aké",4,1),new n("eké",4,1),new n("oké",4,1),new n("áké",4,3),new n("éké",4,2),new n("öké",4,1),new n("éé",3,8)],F=[new n("a",-1,18),new n("ja",0,17),new n("d",-1,16),new n("ad",2,13),new n("ed",2,13),new n("od",2,13),new n("ád",2,14),new n("éd",2,15),new n("öd",2,13),new n("e",-1,18),new n("je",9,17),new n("nk",-1,4),new n("unk",11,1),new n("ánk",11,2),new n("énk",11,3),new n("ünk",11,1),new n("uk",-1,8),new n("juk",16,7),new n("ájuk",17,5),new n("ük",-1,8),new n("jük",19,7),new n("éjük",20,6),new n("m",-1,12),new n("am",22,9),new n("em",22,9),new n("om",22,9),new n("ám",22,10),new n("ém",22,11),new n("o",-1,18),new n("á",-1,19),new n("é",-1,20)],S=[new n("id",-1,10),new n("aid",0,9),new n("jaid",1,6),new n("eid",0,9),new n("jeid",3,6),new n("áid",0,7),new n("éid",0,8),new n("i",-1,15),new n("ai",7,14),new n("jai",8,11),new n("ei",7,14),new n("jei",10,11),new n("ái",7,12),new n("éi",7,13),new n("itek",-1,24),new n("eitek",14,21),new n("jeitek",15,20),new n("éitek",14,23),new n("ik",-1,29),new n("aik",18,26),new n("jaik",19,25),new n("eik",18,26),new n("jeik",21,25),new n("áik",18,27),new n("éik",18,28),new n("ink",-1,20),new n("aink",25,17),new n("jaink",26,16),new n("eink",25,17),new n("jeink",28,16),new n("áink",25,18),new n("éink",25,19),new n("aitok",-1,21),new n("jaitok",32,20),new n("áitok",-1,22),new n("im",-1,5),new n("aim",35,4),new n("jaim",36,1),new n("eim",35,4),new n("jeim",38,1),new n("áim",35,2),new n("éim",35,3)],W=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,1,17,52,14],L=new r;this.setCurrent=function(e){L.setCurrent(e)},this.getCurrent=function(){return L.getCurrent()},this.stem=function(){var n=L.cursor;return e(),L.limit_backward=n,L.cursor=L.limit,c(),L.cursor=L.limit,o(),L.cursor=L.limit,w(),L.cursor=L.limit,l(),L.cursor=L.limit,u(),L.cursor=L.limit,k(),L.cursor=L.limit,f(),L.cursor=L.limit,b(),L.cursor=L.limit,m(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.hu.stemmer,"stemmer-hu"),e.hu.stopWordFilter=e.generateStopWordFilter("a abban ahhoz ahogy ahol aki akik akkor alatt amely amelyek amelyekben amelyeket amelyet amelynek ami amikor amit amolyan amíg annak arra arról az azok azon azonban azt aztán azután azzal azért be belül benne bár cikk cikkek cikkeket csak de e ebben eddig egy egyes egyetlen egyik egyre egyéb egész ehhez ekkor el ellen elsõ elég elõ elõször elõtt emilyen ennek erre ez ezek ezen ezt ezzel ezért fel felé hanem hiszen hogy hogyan igen ill ill. illetve ilyen ilyenkor ismét ison itt jobban jó jól kell kellett keressünk keresztül ki kívül között közül legalább legyen lehet lehetett lenne lenni lesz lett maga magát majd majd meg mellett mely melyek mert mi mikor milyen minden mindenki mindent mindig mint mintha mit mivel miért most már más másik még míg nagy nagyobb nagyon ne nekem neki nem nincs néha néhány nélkül olyan ott pedig persze rá s saját sem semmi sok sokat sokkal szemben szerint szinte számára talán tehát teljes tovább továbbá több ugyanis utolsó után utána vagy vagyis vagyok valaki valami valamint való van vannak vele vissza viszont volna volt voltak voltam voltunk által általában át én éppen és így õ õk õket össze úgy új újabb újra".split(" ")),e.Pipeline.registerFunction(e.hu.stopWordFilter,"stopWordFilter-hu")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.it.min.js b/v2/assets/javascripts/lunr/min/lunr.it.min.js new file mode 100644 index 000000000..344b6a3c0 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.it.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Italian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.it=function(){this.pipeline.reset(),this.pipeline.add(e.it.trimmer,e.it.stopWordFilter,e.it.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.it.stemmer))},e.it.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.it.trimmer=e.trimmerSupport.generateTrimmer(e.it.wordCharacters),e.Pipeline.registerFunction(e.it.trimmer,"trimmer-it"),e.it.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(e,r,n){return!(!x.eq_s(1,e)||(x.ket=x.cursor,!x.in_grouping(L,97,249)))&&(x.slice_from(r),x.cursor=n,!0)}function i(){for(var r,n,i,o,t=x.cursor;;){if(x.bra=x.cursor,r=x.find_among(h,7))switch(x.ket=x.cursor,r){case 1:x.slice_from("à");continue;case 2:x.slice_from("è");continue;case 3:x.slice_from("ì");continue;case 4:x.slice_from("ò");continue;case 5:x.slice_from("ù");continue;case 6:x.slice_from("qU");continue;case 7:if(x.cursor>=x.limit)break;x.cursor++;continue}break}for(x.cursor=t;;)for(n=x.cursor;;){if(i=x.cursor,x.in_grouping(L,97,249)){if(x.bra=x.cursor,o=x.cursor,e("u","U",i))break;if(x.cursor=o,e("i","I",i))break}if(x.cursor=i,x.cursor>=x.limit)return void(x.cursor=n);x.cursor++}}function o(e){if(x.cursor=e,!x.in_grouping(L,97,249))return!1;for(;!x.out_grouping(L,97,249);){if(x.cursor>=x.limit)return!1;x.cursor++}return!0}function t(){if(x.in_grouping(L,97,249)){var e=x.cursor;if(x.out_grouping(L,97,249)){for(;!x.in_grouping(L,97,249);){if(x.cursor>=x.limit)return o(e);x.cursor++}return!0}return o(e)}return!1}function s(){var e,r=x.cursor;if(!t()){if(x.cursor=r,!x.out_grouping(L,97,249))return;if(e=x.cursor,x.out_grouping(L,97,249)){for(;!x.in_grouping(L,97,249);){if(x.cursor>=x.limit)return x.cursor=e,void(x.in_grouping(L,97,249)&&x.cursor=x.limit)return;x.cursor++}k=x.cursor}function a(){for(;!x.in_grouping(L,97,249);){if(x.cursor>=x.limit)return!1;x.cursor++}for(;!x.out_grouping(L,97,249);){if(x.cursor>=x.limit)return!1;x.cursor++}return!0}function u(){var e=x.cursor;k=x.limit,p=k,g=k,s(),x.cursor=e,a()&&(p=x.cursor,a()&&(g=x.cursor))}function c(){for(var e;;){if(x.bra=x.cursor,!(e=x.find_among(q,3)))break;switch(x.ket=x.cursor,e){case 1:x.slice_from("i");break;case 2:x.slice_from("u");break;case 3:if(x.cursor>=x.limit)return;x.cursor++}}}function w(){return k<=x.cursor}function l(){return p<=x.cursor}function m(){return g<=x.cursor}function f(){var e;if(x.ket=x.cursor,x.find_among_b(C,37)&&(x.bra=x.cursor,(e=x.find_among_b(z,5))&&w()))switch(e){case 1:x.slice_del();break;case 2:x.slice_from("e")}}function v(){var e;if(x.ket=x.cursor,!(e=x.find_among_b(S,51)))return!1;switch(x.bra=x.cursor,e){case 1:if(!m())return!1;x.slice_del();break;case 2:if(!m())return!1;x.slice_del(),x.ket=x.cursor,x.eq_s_b(2,"ic")&&(x.bra=x.cursor,m()&&x.slice_del());break;case 3:if(!m())return!1;x.slice_from("log");break;case 4:if(!m())return!1;x.slice_from("u");break;case 5:if(!m())return!1;x.slice_from("ente");break;case 6:if(!w())return!1;x.slice_del();break;case 7:if(!l())return!1;x.slice_del(),x.ket=x.cursor,e=x.find_among_b(P,4),e&&(x.bra=x.cursor,m()&&(x.slice_del(),1==e&&(x.ket=x.cursor,x.eq_s_b(2,"at")&&(x.bra=x.cursor,m()&&x.slice_del()))));break;case 8:if(!m())return!1;x.slice_del(),x.ket=x.cursor,e=x.find_among_b(F,3),e&&(x.bra=x.cursor,1==e&&m()&&x.slice_del());break;case 9:if(!m())return!1;x.slice_del(),x.ket=x.cursor,x.eq_s_b(2,"at")&&(x.bra=x.cursor,m()&&(x.slice_del(),x.ket=x.cursor,x.eq_s_b(2,"ic")&&(x.bra=x.cursor,m()&&x.slice_del())))}return!0}function b(){var e,r;x.cursor>=k&&(r=x.limit_backward,x.limit_backward=k,x.ket=x.cursor,e=x.find_among_b(W,87),e&&(x.bra=x.cursor,1==e&&x.slice_del()),x.limit_backward=r)}function d(){var e=x.limit-x.cursor;if(x.ket=x.cursor,x.in_grouping_b(y,97,242)&&(x.bra=x.cursor,w()&&(x.slice_del(),x.ket=x.cursor,x.eq_s_b(1,"i")&&(x.bra=x.cursor,w()))))return void x.slice_del();x.cursor=x.limit-e}function _(){d(),x.ket=x.cursor,x.eq_s_b(1,"h")&&(x.bra=x.cursor,x.in_grouping_b(U,99,103)&&w()&&x.slice_del())}var g,p,k,h=[new r("",-1,7),new r("qu",0,6),new r("á",0,1),new r("é",0,2),new r("í",0,3),new r("ó",0,4),new r("ú",0,5)],q=[new r("",-1,3),new r("I",0,1),new r("U",0,2)],C=[new r("la",-1,-1),new r("cela",0,-1),new r("gliela",0,-1),new r("mela",0,-1),new r("tela",0,-1),new r("vela",0,-1),new r("le",-1,-1),new r("cele",6,-1),new r("gliele",6,-1),new r("mele",6,-1),new r("tele",6,-1),new r("vele",6,-1),new r("ne",-1,-1),new r("cene",12,-1),new r("gliene",12,-1),new r("mene",12,-1),new r("sene",12,-1),new r("tene",12,-1),new r("vene",12,-1),new r("ci",-1,-1),new r("li",-1,-1),new r("celi",20,-1),new r("glieli",20,-1),new r("meli",20,-1),new r("teli",20,-1),new r("veli",20,-1),new r("gli",20,-1),new r("mi",-1,-1),new r("si",-1,-1),new r("ti",-1,-1),new r("vi",-1,-1),new r("lo",-1,-1),new r("celo",31,-1),new r("glielo",31,-1),new r("melo",31,-1),new r("telo",31,-1),new r("velo",31,-1)],z=[new r("ando",-1,1),new r("endo",-1,1),new r("ar",-1,2),new r("er",-1,2),new r("ir",-1,2)],P=[new r("ic",-1,-1),new r("abil",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],F=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],S=[new r("ica",-1,1),new r("logia",-1,3),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,9),new r("anza",-1,1),new r("enza",-1,5),new r("ice",-1,1),new r("atrice",7,1),new r("iche",-1,1),new r("logie",-1,3),new r("abile",-1,1),new r("ibile",-1,1),new r("usione",-1,4),new r("azione",-1,2),new r("uzione",-1,4),new r("atore",-1,2),new r("ose",-1,1),new r("ante",-1,1),new r("mente",-1,1),new r("amente",19,7),new r("iste",-1,1),new r("ive",-1,9),new r("anze",-1,1),new r("enze",-1,5),new r("ici",-1,1),new r("atrici",25,1),new r("ichi",-1,1),new r("abili",-1,1),new r("ibili",-1,1),new r("ismi",-1,1),new r("usioni",-1,4),new r("azioni",-1,2),new r("uzioni",-1,4),new r("atori",-1,2),new r("osi",-1,1),new r("anti",-1,1),new r("amenti",-1,6),new r("imenti",-1,6),new r("isti",-1,1),new r("ivi",-1,9),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,6),new r("imento",-1,6),new r("ivo",-1,9),new r("ità",-1,8),new r("istà",-1,1),new r("istè",-1,1),new r("istì",-1,1)],W=[new r("isca",-1,1),new r("enda",-1,1),new r("ata",-1,1),new r("ita",-1,1),new r("uta",-1,1),new r("ava",-1,1),new r("eva",-1,1),new r("iva",-1,1),new r("erebbe",-1,1),new r("irebbe",-1,1),new r("isce",-1,1),new r("ende",-1,1),new r("are",-1,1),new r("ere",-1,1),new r("ire",-1,1),new r("asse",-1,1),new r("ate",-1,1),new r("avate",16,1),new r("evate",16,1),new r("ivate",16,1),new r("ete",-1,1),new r("erete",20,1),new r("irete",20,1),new r("ite",-1,1),new r("ereste",-1,1),new r("ireste",-1,1),new r("ute",-1,1),new r("erai",-1,1),new r("irai",-1,1),new r("isci",-1,1),new r("endi",-1,1),new r("erei",-1,1),new r("irei",-1,1),new r("assi",-1,1),new r("ati",-1,1),new r("iti",-1,1),new r("eresti",-1,1),new r("iresti",-1,1),new r("uti",-1,1),new r("avi",-1,1),new r("evi",-1,1),new r("ivi",-1,1),new r("isco",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("Yamo",-1,1),new r("iamo",-1,1),new r("avamo",-1,1),new r("evamo",-1,1),new r("ivamo",-1,1),new r("eremo",-1,1),new r("iremo",-1,1),new r("assimo",-1,1),new r("ammo",-1,1),new r("emmo",-1,1),new r("eremmo",54,1),new r("iremmo",54,1),new r("immo",-1,1),new r("ano",-1,1),new r("iscano",58,1),new r("avano",58,1),new r("evano",58,1),new r("ivano",58,1),new r("eranno",-1,1),new r("iranno",-1,1),new r("ono",-1,1),new r("iscono",65,1),new r("arono",65,1),new r("erono",65,1),new r("irono",65,1),new r("erebbero",-1,1),new r("irebbero",-1,1),new r("assero",-1,1),new r("essero",-1,1),new r("issero",-1,1),new r("ato",-1,1),new r("ito",-1,1),new r("uto",-1,1),new r("avo",-1,1),new r("evo",-1,1),new r("ivo",-1,1),new r("ar",-1,1),new r("ir",-1,1),new r("erà",-1,1),new r("irà",-1,1),new r("erò",-1,1),new r("irò",-1,1)],L=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,128,128,8,2,1],y=[17,65,0,0,0,0,0,0,0,0,0,0,0,0,0,128,128,8,2],U=[17],x=new n;this.setCurrent=function(e){x.setCurrent(e)},this.getCurrent=function(){return x.getCurrent()},this.stem=function(){var e=x.cursor;return i(),x.cursor=e,u(),x.limit_backward=e,x.cursor=x.limit,f(),x.cursor=x.limit,v()||(x.cursor=x.limit,b()),x.cursor=x.limit,_(),x.cursor=x.limit_backward,c(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.it.stemmer,"stemmer-it"),e.it.stopWordFilter=e.generateStopWordFilter("a abbia abbiamo abbiano abbiate ad agl agli ai al all alla alle allo anche avemmo avendo avesse avessero avessi avessimo aveste avesti avete aveva avevamo avevano avevate avevi avevo avrai avranno avrebbe avrebbero avrei avremmo avremo avreste avresti avrete avrà avrò avuta avute avuti avuto c che chi ci coi col come con contro cui da dagl dagli dai dal dall dalla dalle dallo degl degli dei del dell della delle dello di dov dove e ebbe ebbero ebbi ed era erano eravamo eravate eri ero essendo faccia facciamo facciano facciate faccio facemmo facendo facesse facessero facessi facessimo faceste facesti faceva facevamo facevano facevate facevi facevo fai fanno farai faranno farebbe farebbero farei faremmo faremo fareste faresti farete farà farò fece fecero feci fosse fossero fossi fossimo foste fosti fu fui fummo furono gli ha hai hanno ho i il in io l la le lei li lo loro lui ma mi mia mie miei mio ne negl negli nei nel nell nella nelle nello noi non nostra nostre nostri nostro o per perché più quale quanta quante quanti quanto quella quelle quelli quello questa queste questi questo sarai saranno sarebbe sarebbero sarei saremmo saremo sareste saresti sarete sarà sarò se sei si sia siamo siano siate siete sono sta stai stando stanno starai staranno starebbe starebbero starei staremmo staremo stareste staresti starete starà starò stava stavamo stavano stavate stavi stavo stemmo stesse stessero stessi stessimo steste stesti stette stettero stetti stia stiamo stiano stiate sto su sua sue sugl sugli sui sul sull sulla sulle sullo suo suoi ti tra tu tua tue tuo tuoi tutti tutto un una uno vi voi vostra vostre vostri vostro è".split(" ")),e.Pipeline.registerFunction(e.it.stopWordFilter,"stopWordFilter-it")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.ja.min.js b/v2/assets/javascripts/lunr/min/lunr.ja.min.js new file mode 100644 index 000000000..5f254ebe9 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.ja.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.ja=function(){this.pipeline.reset(),this.pipeline.add(e.ja.trimmer,e.ja.stopWordFilter,e.ja.stemmer),r?this.tokenizer=e.ja.tokenizer:(e.tokenizer&&(e.tokenizer=e.ja.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.ja.tokenizer))};var t=new e.TinySegmenter;e.ja.tokenizer=function(i){var n,o,s,p,a,u,m,l,c,f;if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(o=i.toString().toLowerCase().replace(/^\s+/,""),n=o.length-1;n>=0;n--)if(/\S/.test(o.charAt(n))){o=o.substring(0,n+1);break}for(a=[],s=o.length,c=0,l=0;c<=s;c++)if(u=o.charAt(c),m=c-l,u.match(/\s/)||c==s){if(m>0)for(p=t.segment(o.slice(l,c)).filter(function(e){return!!e}),f=l,n=0;n=C.limit)break;C.cursor++;continue}break}for(C.cursor=o,C.bra=o,C.eq_s(1,"y")?(C.ket=C.cursor,C.slice_from("Y")):C.cursor=o;;)if(e=C.cursor,C.in_grouping(q,97,232)){if(i=C.cursor,C.bra=i,C.eq_s(1,"i"))C.ket=C.cursor,C.in_grouping(q,97,232)&&(C.slice_from("I"),C.cursor=e);else if(C.cursor=i,C.eq_s(1,"y"))C.ket=C.cursor,C.slice_from("Y"),C.cursor=e;else if(n(e))break}else if(n(e))break}function n(r){return C.cursor=r,r>=C.limit||(C.cursor++,!1)}function o(){_=C.limit,d=_,t()||(_=C.cursor,_<3&&(_=3),t()||(d=C.cursor))}function t(){for(;!C.in_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}for(;!C.out_grouping(q,97,232);){if(C.cursor>=C.limit)return!0;C.cursor++}return!1}function s(){for(var r;;)if(C.bra=C.cursor,r=C.find_among(p,3))switch(C.ket=C.cursor,r){case 1:C.slice_from("y");break;case 2:C.slice_from("i");break;case 3:if(C.cursor>=C.limit)return;C.cursor++}}function u(){return _<=C.cursor}function c(){return d<=C.cursor}function a(){var r=C.limit-C.cursor;C.find_among_b(g,3)&&(C.cursor=C.limit-r,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del()))}function l(){var r;w=!1,C.ket=C.cursor,C.eq_s_b(1,"e")&&(C.bra=C.cursor,u()&&(r=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-r,C.slice_del(),w=!0,a())))}function m(){var r;u()&&(r=C.limit-C.cursor,C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-r,C.eq_s_b(3,"gem")||(C.cursor=C.limit-r,C.slice_del(),a())))}function f(){var r,e,i,n,o,t,s=C.limit-C.cursor;if(C.ket=C.cursor,r=C.find_among_b(h,5))switch(C.bra=C.cursor,r){case 1:u()&&C.slice_from("heid");break;case 2:m();break;case 3:u()&&C.out_grouping_b(j,97,232)&&C.slice_del()}if(C.cursor=C.limit-s,l(),C.cursor=C.limit-s,C.ket=C.cursor,C.eq_s_b(4,"heid")&&(C.bra=C.cursor,c()&&(e=C.limit-C.cursor,C.eq_s_b(1,"c")||(C.cursor=C.limit-e,C.slice_del(),C.ket=C.cursor,C.eq_s_b(2,"en")&&(C.bra=C.cursor,m())))),C.cursor=C.limit-s,C.ket=C.cursor,r=C.find_among_b(k,6))switch(C.bra=C.cursor,r){case 1:if(c()){if(C.slice_del(),i=C.limit-C.cursor,C.ket=C.cursor,C.eq_s_b(2,"ig")&&(C.bra=C.cursor,c()&&(n=C.limit-C.cursor,!C.eq_s_b(1,"e")))){C.cursor=C.limit-n,C.slice_del();break}C.cursor=C.limit-i,a()}break;case 2:c()&&(o=C.limit-C.cursor,C.eq_s_b(1,"e")||(C.cursor=C.limit-o,C.slice_del()));break;case 3:c()&&(C.slice_del(),l());break;case 4:c()&&C.slice_del();break;case 5:c()&&w&&C.slice_del()}C.cursor=C.limit-s,C.out_grouping_b(z,73,232)&&(t=C.limit-C.cursor,C.find_among_b(v,4)&&C.out_grouping_b(q,97,232)&&(C.cursor=C.limit-t,C.ket=C.cursor,C.cursor>C.limit_backward&&(C.cursor--,C.bra=C.cursor,C.slice_del())))}var d,_,w,b=[new e("",-1,6),new e("á",0,1),new e("ä",0,1),new e("é",0,2),new e("ë",0,2),new e("í",0,3),new e("ï",0,3),new e("ó",0,4),new e("ö",0,4),new e("ú",0,5),new e("ü",0,5)],p=[new e("",-1,3),new e("I",0,2),new e("Y",0,1)],g=[new e("dd",-1,-1),new e("kk",-1,-1),new e("tt",-1,-1)],h=[new e("ene",-1,2),new e("se",-1,3),new e("en",-1,2),new e("heden",2,1),new e("s",-1,3)],k=[new e("end",-1,1),new e("ig",-1,2),new e("ing",-1,1),new e("lijk",-1,3),new e("baar",-1,4),new e("bar",-1,5)],v=[new e("aa",-1,-1),new e("ee",-1,-1),new e("oo",-1,-1),new e("uu",-1,-1)],q=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],z=[1,0,0,17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],j=[17,67,16,1,0,0,0,0,0,0,0,0,0,0,0,0,128],C=new i;this.setCurrent=function(r){C.setCurrent(r)},this.getCurrent=function(){return C.getCurrent()},this.stem=function(){var e=C.cursor;return r(),C.cursor=e,o(),C.limit_backward=e,C.cursor=C.limit,f(),C.cursor=C.limit_backward,s(),!0}};return function(r){return"function"==typeof r.update?r.update(function(r){return n.setCurrent(r),n.stem(),n.getCurrent()}):(n.setCurrent(r),n.stem(),n.getCurrent())}}(),r.Pipeline.registerFunction(r.nl.stemmer,"stemmer-nl"),r.nl.stopWordFilter=r.generateStopWordFilter(" aan al alles als altijd andere ben bij daar dan dat de der deze die dit doch doen door dus een eens en er ge geen geweest haar had heb hebben heeft hem het hier hij hoe hun iemand iets ik in is ja je kan kon kunnen maar me meer men met mij mijn moet na naar niet niets nog nu of om omdat onder ons ook op over reeds te tegen toch toen tot u uit uw van veel voor want waren was wat werd wezen wie wil worden wordt zal ze zelf zich zij zijn zo zonder zou".split(" ")),r.Pipeline.registerFunction(r.nl.stopWordFilter,"stopWordFilter-nl")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.no.min.js b/v2/assets/javascripts/lunr/min/lunr.no.min.js new file mode 100644 index 000000000..92bc7e4e8 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.no.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Norwegian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.no=function(){this.pipeline.reset(),this.pipeline.add(e.no.trimmer,e.no.stopWordFilter,e.no.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.no.stemmer))},e.no.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.no.trimmer=e.trimmerSupport.generateTrimmer(e.no.wordCharacters),e.Pipeline.registerFunction(e.no.trimmer,"trimmer-no"),e.no.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,r=w.cursor+3;if(a=w.limit,0<=r||r<=w.limit){for(s=r;;){if(e=w.cursor,w.in_grouping(d,97,248)){w.cursor=e;break}if(e>=w.limit)return;w.cursor=e+1}for(;!w.out_grouping(d,97,248);){if(w.cursor>=w.limit)return;w.cursor++}a=w.cursor,a=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.pt.min.js b/v2/assets/javascripts/lunr/min/lunr.pt.min.js new file mode 100644 index 000000000..6c16996d6 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.pt.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Portuguese` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.pt=function(){this.pipeline.reset(),this.pipeline.add(e.pt.trimmer,e.pt.stopWordFilter,e.pt.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.pt.stemmer))},e.pt.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.pt.trimmer=e.trimmerSupport.generateTrimmer(e.pt.wordCharacters),e.Pipeline.registerFunction(e.pt.trimmer,"trimmer-pt"),e.pt.stemmer=function(){var r=e.stemmerSupport.Among,s=e.stemmerSupport.SnowballProgram,n=new function(){function e(){for(var e;;){if(z.bra=z.cursor,e=z.find_among(k,3))switch(z.ket=z.cursor,e){case 1:z.slice_from("a~");continue;case 2:z.slice_from("o~");continue;case 3:if(z.cursor>=z.limit)break;z.cursor++;continue}break}}function n(){if(z.out_grouping(y,97,250)){for(;!z.in_grouping(y,97,250);){if(z.cursor>=z.limit)return!0;z.cursor++}return!1}return!0}function i(){if(z.in_grouping(y,97,250))for(;!z.out_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}return g=z.cursor,!0}function o(){var e,r,s=z.cursor;if(z.in_grouping(y,97,250))if(e=z.cursor,n()){if(z.cursor=e,i())return}else g=z.cursor;if(z.cursor=s,z.out_grouping(y,97,250)){if(r=z.cursor,n()){if(z.cursor=r,!z.in_grouping(y,97,250)||z.cursor>=z.limit)return;z.cursor++}g=z.cursor}}function t(){for(;!z.in_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}for(;!z.out_grouping(y,97,250);){if(z.cursor>=z.limit)return!1;z.cursor++}return!0}function a(){var e=z.cursor;g=z.limit,b=g,h=g,o(),z.cursor=e,t()&&(b=z.cursor,t()&&(h=z.cursor))}function u(){for(var e;;){if(z.bra=z.cursor,e=z.find_among(q,3))switch(z.ket=z.cursor,e){case 1:z.slice_from("ã");continue;case 2:z.slice_from("õ");continue;case 3:if(z.cursor>=z.limit)break;z.cursor++;continue}break}}function w(){return g<=z.cursor}function m(){return b<=z.cursor}function c(){return h<=z.cursor}function l(){var e;if(z.ket=z.cursor,!(e=z.find_among_b(F,45)))return!1;switch(z.bra=z.cursor,e){case 1:if(!c())return!1;z.slice_del();break;case 2:if(!c())return!1;z.slice_from("log");break;case 3:if(!c())return!1;z.slice_from("u");break;case 4:if(!c())return!1;z.slice_from("ente");break;case 5:if(!m())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(j,4),e&&(z.bra=z.cursor,c()&&(z.slice_del(),1==e&&(z.ket=z.cursor,z.eq_s_b(2,"at")&&(z.bra=z.cursor,c()&&z.slice_del()))));break;case 6:if(!c())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(C,3),e&&(z.bra=z.cursor,1==e&&c()&&z.slice_del());break;case 7:if(!c())return!1;z.slice_del(),z.ket=z.cursor,e=z.find_among_b(P,3),e&&(z.bra=z.cursor,1==e&&c()&&z.slice_del());break;case 8:if(!c())return!1;z.slice_del(),z.ket=z.cursor,z.eq_s_b(2,"at")&&(z.bra=z.cursor,c()&&z.slice_del());break;case 9:if(!w()||!z.eq_s_b(1,"e"))return!1;z.slice_from("ir")}return!0}function f(){var e,r;if(z.cursor>=g){if(r=z.limit_backward,z.limit_backward=g,z.ket=z.cursor,e=z.find_among_b(S,120))return z.bra=z.cursor,1==e&&z.slice_del(),z.limit_backward=r,!0;z.limit_backward=r}return!1}function d(){var e;z.ket=z.cursor,(e=z.find_among_b(W,7))&&(z.bra=z.cursor,1==e&&w()&&z.slice_del())}function v(e,r){if(z.eq_s_b(1,e)){z.bra=z.cursor;var s=z.limit-z.cursor;if(z.eq_s_b(1,r))return z.cursor=z.limit-s,w()&&z.slice_del(),!1}return!0}function p(){var e;if(z.ket=z.cursor,e=z.find_among_b(L,4))switch(z.bra=z.cursor,e){case 1:w()&&(z.slice_del(),z.ket=z.cursor,z.limit-z.cursor,v("u","g")&&v("i","c"));break;case 2:z.slice_from("c")}}function _(){if(!l()&&(z.cursor=z.limit,!f()))return z.cursor=z.limit,void d();z.cursor=z.limit,z.ket=z.cursor,z.eq_s_b(1,"i")&&(z.bra=z.cursor,z.eq_s_b(1,"c")&&(z.cursor=z.limit,w()&&z.slice_del()))}var h,b,g,k=[new r("",-1,3),new r("ã",0,1),new r("õ",0,2)],q=[new r("",-1,3),new r("a~",0,1),new r("o~",0,2)],j=[new r("ic",-1,-1),new r("ad",-1,-1),new r("os",-1,-1),new r("iv",-1,1)],C=[new r("ante",-1,1),new r("avel",-1,1),new r("ível",-1,1)],P=[new r("ic",-1,1),new r("abil",-1,1),new r("iv",-1,1)],F=[new r("ica",-1,1),new r("ância",-1,1),new r("ência",-1,4),new r("ira",-1,9),new r("adora",-1,1),new r("osa",-1,1),new r("ista",-1,1),new r("iva",-1,8),new r("eza",-1,1),new r("logía",-1,2),new r("idade",-1,7),new r("ante",-1,1),new r("mente",-1,6),new r("amente",12,5),new r("ável",-1,1),new r("ível",-1,1),new r("ución",-1,3),new r("ico",-1,1),new r("ismo",-1,1),new r("oso",-1,1),new r("amento",-1,1),new r("imento",-1,1),new r("ivo",-1,8),new r("aça~o",-1,1),new r("ador",-1,1),new r("icas",-1,1),new r("ências",-1,4),new r("iras",-1,9),new r("adoras",-1,1),new r("osas",-1,1),new r("istas",-1,1),new r("ivas",-1,8),new r("ezas",-1,1),new r("logías",-1,2),new r("idades",-1,7),new r("uciones",-1,3),new r("adores",-1,1),new r("antes",-1,1),new r("aço~es",-1,1),new r("icos",-1,1),new r("ismos",-1,1),new r("osos",-1,1),new r("amentos",-1,1),new r("imentos",-1,1),new r("ivos",-1,8)],S=[new r("ada",-1,1),new r("ida",-1,1),new r("ia",-1,1),new r("aria",2,1),new r("eria",2,1),new r("iria",2,1),new r("ara",-1,1),new r("era",-1,1),new r("ira",-1,1),new r("ava",-1,1),new r("asse",-1,1),new r("esse",-1,1),new r("isse",-1,1),new r("aste",-1,1),new r("este",-1,1),new r("iste",-1,1),new r("ei",-1,1),new r("arei",16,1),new r("erei",16,1),new r("irei",16,1),new r("am",-1,1),new r("iam",20,1),new r("ariam",21,1),new r("eriam",21,1),new r("iriam",21,1),new r("aram",20,1),new r("eram",20,1),new r("iram",20,1),new r("avam",20,1),new r("em",-1,1),new r("arem",29,1),new r("erem",29,1),new r("irem",29,1),new r("assem",29,1),new r("essem",29,1),new r("issem",29,1),new r("ado",-1,1),new r("ido",-1,1),new r("ando",-1,1),new r("endo",-1,1),new r("indo",-1,1),new r("ara~o",-1,1),new r("era~o",-1,1),new r("ira~o",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("ir",-1,1),new r("as",-1,1),new r("adas",47,1),new r("idas",47,1),new r("ias",47,1),new r("arias",50,1),new r("erias",50,1),new r("irias",50,1),new r("aras",47,1),new r("eras",47,1),new r("iras",47,1),new r("avas",47,1),new r("es",-1,1),new r("ardes",58,1),new r("erdes",58,1),new r("irdes",58,1),new r("ares",58,1),new r("eres",58,1),new r("ires",58,1),new r("asses",58,1),new r("esses",58,1),new r("isses",58,1),new r("astes",58,1),new r("estes",58,1),new r("istes",58,1),new r("is",-1,1),new r("ais",71,1),new r("eis",71,1),new r("areis",73,1),new r("ereis",73,1),new r("ireis",73,1),new r("áreis",73,1),new r("éreis",73,1),new r("íreis",73,1),new r("ásseis",73,1),new r("ésseis",73,1),new r("ísseis",73,1),new r("áveis",73,1),new r("íeis",73,1),new r("aríeis",84,1),new r("eríeis",84,1),new r("iríeis",84,1),new r("ados",-1,1),new r("idos",-1,1),new r("amos",-1,1),new r("áramos",90,1),new r("éramos",90,1),new r("íramos",90,1),new r("ávamos",90,1),new r("íamos",90,1),new r("aríamos",95,1),new r("eríamos",95,1),new r("iríamos",95,1),new r("emos",-1,1),new r("aremos",99,1),new r("eremos",99,1),new r("iremos",99,1),new r("ássemos",99,1),new r("êssemos",99,1),new r("íssemos",99,1),new r("imos",-1,1),new r("armos",-1,1),new r("ermos",-1,1),new r("irmos",-1,1),new r("ámos",-1,1),new r("arás",-1,1),new r("erás",-1,1),new r("irás",-1,1),new r("eu",-1,1),new r("iu",-1,1),new r("ou",-1,1),new r("ará",-1,1),new r("erá",-1,1),new r("irá",-1,1)],W=[new r("a",-1,1),new r("i",-1,1),new r("o",-1,1),new r("os",-1,1),new r("á",-1,1),new r("í",-1,1),new r("ó",-1,1)],L=[new r("e",-1,1),new r("ç",-1,2),new r("é",-1,1),new r("ê",-1,1)],y=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,3,19,12,2],z=new s;this.setCurrent=function(e){z.setCurrent(e)},this.getCurrent=function(){return z.getCurrent()},this.stem=function(){var r=z.cursor;return e(),z.cursor=r,a(),z.limit_backward=r,z.cursor=z.limit,_(),z.cursor=z.limit,p(),z.cursor=z.limit_backward,u(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.pt.stemmer,"stemmer-pt"),e.pt.stopWordFilter=e.generateStopWordFilter("a ao aos aquela aquelas aquele aqueles aquilo as até com como da das de dela delas dele deles depois do dos e ela elas ele eles em entre era eram essa essas esse esses esta estamos estas estava estavam este esteja estejam estejamos estes esteve estive estivemos estiver estivera estiveram estiverem estivermos estivesse estivessem estivéramos estivéssemos estou está estávamos estão eu foi fomos for fora foram forem formos fosse fossem fui fôramos fôssemos haja hajam hajamos havemos hei houve houvemos houver houvera houveram houverei houverem houveremos houveria houveriam houvermos houverá houverão houveríamos houvesse houvessem houvéramos houvéssemos há hão isso isto já lhe lhes mais mas me mesmo meu meus minha minhas muito na nas nem no nos nossa nossas nosso nossos num numa não nós o os ou para pela pelas pelo pelos por qual quando que quem se seja sejam sejamos sem serei seremos seria seriam será serão seríamos seu seus somos sou sua suas são só também te tem temos tenha tenham tenhamos tenho terei teremos teria teriam terá terão teríamos teu teus teve tinha tinham tive tivemos tiver tivera tiveram tiverem tivermos tivesse tivessem tivéramos tivéssemos tu tua tuas tém tínhamos um uma você vocês vos à às éramos".split(" ")),e.Pipeline.registerFunction(e.pt.stopWordFilter,"stopWordFilter-pt")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.ro.min.js b/v2/assets/javascripts/lunr/min/lunr.ro.min.js new file mode 100644 index 000000000..727714018 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.ro.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Romanian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ro=function(){this.pipeline.reset(),this.pipeline.add(e.ro.trimmer,e.ro.stopWordFilter,e.ro.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ro.stemmer))},e.ro.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.ro.trimmer=e.trimmerSupport.generateTrimmer(e.ro.wordCharacters),e.Pipeline.registerFunction(e.ro.trimmer,"trimmer-ro"),e.ro.stemmer=function(){var i=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,n=new function(){function e(e,i){L.eq_s(1,e)&&(L.ket=L.cursor,L.in_grouping(W,97,259)&&L.slice_from(i))}function n(){for(var i,r;;){if(i=L.cursor,L.in_grouping(W,97,259)&&(r=L.cursor,L.bra=r,e("u","U"),L.cursor=r,e("i","I")),L.cursor=i,L.cursor>=L.limit)break;L.cursor++}}function t(){if(L.out_grouping(W,97,259)){for(;!L.in_grouping(W,97,259);){if(L.cursor>=L.limit)return!0;L.cursor++}return!1}return!0}function a(){if(L.in_grouping(W,97,259))for(;!L.out_grouping(W,97,259);){if(L.cursor>=L.limit)return!0;L.cursor++}return!1}function o(){var e,i,r=L.cursor;if(L.in_grouping(W,97,259)){if(e=L.cursor,!t())return void(h=L.cursor);if(L.cursor=e,!a())return void(h=L.cursor)}L.cursor=r,L.out_grouping(W,97,259)&&(i=L.cursor,t()&&(L.cursor=i,L.in_grouping(W,97,259)&&L.cursor=L.limit)return!1;L.cursor++}for(;!L.out_grouping(W,97,259);){if(L.cursor>=L.limit)return!1;L.cursor++}return!0}function c(){var e=L.cursor;h=L.limit,k=h,g=h,o(),L.cursor=e,u()&&(k=L.cursor,u()&&(g=L.cursor))}function s(){for(var e;;){if(L.bra=L.cursor,e=L.find_among(z,3))switch(L.ket=L.cursor,e){case 1:L.slice_from("i");continue;case 2:L.slice_from("u");continue;case 3:if(L.cursor>=L.limit)break;L.cursor++;continue}break}}function w(){return h<=L.cursor}function m(){return k<=L.cursor}function l(){return g<=L.cursor}function f(){var e,i;if(L.ket=L.cursor,(e=L.find_among_b(C,16))&&(L.bra=L.cursor,m()))switch(e){case 1:L.slice_del();break;case 2:L.slice_from("a");break;case 3:L.slice_from("e");break;case 4:L.slice_from("i");break;case 5:i=L.limit-L.cursor,L.eq_s_b(2,"ab")||(L.cursor=L.limit-i,L.slice_from("i"));break;case 6:L.slice_from("at");break;case 7:L.slice_from("aţi")}}function p(){var e,i=L.limit-L.cursor;if(L.ket=L.cursor,(e=L.find_among_b(P,46))&&(L.bra=L.cursor,m())){switch(e){case 1:L.slice_from("abil");break;case 2:L.slice_from("ibil");break;case 3:L.slice_from("iv");break;case 4:L.slice_from("ic");break;case 5:L.slice_from("at");break;case 6:L.slice_from("it")}return _=!0,L.cursor=L.limit-i,!0}return!1}function d(){var e,i;for(_=!1;;)if(i=L.limit-L.cursor,!p()){L.cursor=L.limit-i;break}if(L.ket=L.cursor,(e=L.find_among_b(F,62))&&(L.bra=L.cursor,l())){switch(e){case 1:L.slice_del();break;case 2:L.eq_s_b(1,"ţ")&&(L.bra=L.cursor,L.slice_from("t"));break;case 3:L.slice_from("ist")}_=!0}}function b(){var e,i,r;if(L.cursor>=h){if(i=L.limit_backward,L.limit_backward=h,L.ket=L.cursor,e=L.find_among_b(q,94))switch(L.bra=L.cursor,e){case 1:if(r=L.limit-L.cursor,!L.out_grouping_b(W,97,259)&&(L.cursor=L.limit-r,!L.eq_s_b(1,"u")))break;case 2:L.slice_del()}L.limit_backward=i}}function v(){var e;L.ket=L.cursor,(e=L.find_among_b(S,5))&&(L.bra=L.cursor,w()&&1==e&&L.slice_del())}var _,g,k,h,z=[new i("",-1,3),new i("I",0,1),new i("U",0,2)],C=[new i("ea",-1,3),new i("aţia",-1,7),new i("aua",-1,2),new i("iua",-1,4),new i("aţie",-1,7),new i("ele",-1,3),new i("ile",-1,5),new i("iile",6,4),new i("iei",-1,4),new i("atei",-1,6),new i("ii",-1,4),new i("ului",-1,1),new i("ul",-1,1),new i("elor",-1,3),new i("ilor",-1,4),new i("iilor",14,4)],P=[new i("icala",-1,4),new i("iciva",-1,4),new i("ativa",-1,5),new i("itiva",-1,6),new i("icale",-1,4),new i("aţiune",-1,5),new i("iţiune",-1,6),new i("atoare",-1,5),new i("itoare",-1,6),new i("ătoare",-1,5),new i("icitate",-1,4),new i("abilitate",-1,1),new i("ibilitate",-1,2),new i("ivitate",-1,3),new i("icive",-1,4),new i("ative",-1,5),new i("itive",-1,6),new i("icali",-1,4),new i("atori",-1,5),new i("icatori",18,4),new i("itori",-1,6),new i("ători",-1,5),new i("icitati",-1,4),new i("abilitati",-1,1),new i("ivitati",-1,3),new i("icivi",-1,4),new i("ativi",-1,5),new i("itivi",-1,6),new i("icităi",-1,4),new i("abilităi",-1,1),new i("ivităi",-1,3),new i("icităţi",-1,4),new i("abilităţi",-1,1),new i("ivităţi",-1,3),new i("ical",-1,4),new i("ator",-1,5),new i("icator",35,4),new i("itor",-1,6),new i("ător",-1,5),new i("iciv",-1,4),new i("ativ",-1,5),new i("itiv",-1,6),new i("icală",-1,4),new i("icivă",-1,4),new i("ativă",-1,5),new i("itivă",-1,6)],F=[new i("ica",-1,1),new i("abila",-1,1),new i("ibila",-1,1),new i("oasa",-1,1),new i("ata",-1,1),new i("ita",-1,1),new i("anta",-1,1),new i("ista",-1,3),new i("uta",-1,1),new i("iva",-1,1),new i("ic",-1,1),new i("ice",-1,1),new i("abile",-1,1),new i("ibile",-1,1),new i("isme",-1,3),new i("iune",-1,2),new i("oase",-1,1),new i("ate",-1,1),new i("itate",17,1),new i("ite",-1,1),new i("ante",-1,1),new i("iste",-1,3),new i("ute",-1,1),new i("ive",-1,1),new i("ici",-1,1),new i("abili",-1,1),new i("ibili",-1,1),new i("iuni",-1,2),new i("atori",-1,1),new i("osi",-1,1),new i("ati",-1,1),new i("itati",30,1),new i("iti",-1,1),new i("anti",-1,1),new i("isti",-1,3),new i("uti",-1,1),new i("işti",-1,3),new i("ivi",-1,1),new i("ităi",-1,1),new i("oşi",-1,1),new i("ităţi",-1,1),new i("abil",-1,1),new i("ibil",-1,1),new i("ism",-1,3),new i("ator",-1,1),new i("os",-1,1),new i("at",-1,1),new i("it",-1,1),new i("ant",-1,1),new i("ist",-1,3),new i("ut",-1,1),new i("iv",-1,1),new i("ică",-1,1),new i("abilă",-1,1),new i("ibilă",-1,1),new i("oasă",-1,1),new i("ată",-1,1),new i("ită",-1,1),new i("antă",-1,1),new i("istă",-1,3),new i("ută",-1,1),new i("ivă",-1,1)],q=[new i("ea",-1,1),new i("ia",-1,1),new i("esc",-1,1),new i("ăsc",-1,1),new i("ind",-1,1),new i("ând",-1,1),new i("are",-1,1),new i("ere",-1,1),new i("ire",-1,1),new i("âre",-1,1),new i("se",-1,2),new i("ase",10,1),new i("sese",10,2),new i("ise",10,1),new i("use",10,1),new i("âse",10,1),new i("eşte",-1,1),new i("ăşte",-1,1),new i("eze",-1,1),new i("ai",-1,1),new i("eai",19,1),new i("iai",19,1),new i("sei",-1,2),new i("eşti",-1,1),new i("ăşti",-1,1),new i("ui",-1,1),new i("ezi",-1,1),new i("âi",-1,1),new i("aşi",-1,1),new i("seşi",-1,2),new i("aseşi",29,1),new i("seseşi",29,2),new i("iseşi",29,1),new i("useşi",29,1),new i("âseşi",29,1),new i("işi",-1,1),new i("uşi",-1,1),new i("âşi",-1,1),new i("aţi",-1,2),new i("eaţi",38,1),new i("iaţi",38,1),new i("eţi",-1,2),new i("iţi",-1,2),new i("âţi",-1,2),new i("arăţi",-1,1),new i("serăţi",-1,2),new i("aserăţi",45,1),new i("seserăţi",45,2),new i("iserăţi",45,1),new i("userăţi",45,1),new i("âserăţi",45,1),new i("irăţi",-1,1),new i("urăţi",-1,1),new i("ârăţi",-1,1),new i("am",-1,1),new i("eam",54,1),new i("iam",54,1),new i("em",-1,2),new i("asem",57,1),new i("sesem",57,2),new i("isem",57,1),new i("usem",57,1),new i("âsem",57,1),new i("im",-1,2),new i("âm",-1,2),new i("ăm",-1,2),new i("arăm",65,1),new i("serăm",65,2),new i("aserăm",67,1),new i("seserăm",67,2),new i("iserăm",67,1),new i("userăm",67,1),new i("âserăm",67,1),new i("irăm",65,1),new i("urăm",65,1),new i("ârăm",65,1),new i("au",-1,1),new i("eau",76,1),new i("iau",76,1),new i("indu",-1,1),new i("ându",-1,1),new i("ez",-1,1),new i("ească",-1,1),new i("ară",-1,1),new i("seră",-1,2),new i("aseră",84,1),new i("seseră",84,2),new i("iseră",84,1),new i("useră",84,1),new i("âseră",84,1),new i("iră",-1,1),new i("ură",-1,1),new i("âră",-1,1),new i("ează",-1,1)],S=[new i("a",-1,1),new i("e",-1,1),new i("ie",1,1),new i("i",-1,1),new i("ă",-1,1)],W=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,2,32,0,0,4],L=new r;this.setCurrent=function(e){L.setCurrent(e)},this.getCurrent=function(){return L.getCurrent()},this.stem=function(){var e=L.cursor;return n(),L.cursor=e,c(),L.limit_backward=e,L.cursor=L.limit,f(),L.cursor=L.limit,d(),L.cursor=L.limit,_||(L.cursor=L.limit,b(),L.cursor=L.limit),v(),L.cursor=L.limit_backward,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.ro.stemmer,"stemmer-ro"),e.ro.stopWordFilter=e.generateStopWordFilter("acea aceasta această aceea acei aceia acel acela acele acelea acest acesta aceste acestea aceşti aceştia acolo acord acum ai aia aibă aici al ale alea altceva altcineva am ar are asemenea asta astea astăzi asupra au avea avem aveţi azi aş aşadar aţi bine bucur bună ca care caut ce cel ceva chiar cinci cine cineva contra cu cum cumva curând curînd când cât câte câtva câţi cînd cît cîte cîtva cîţi că căci cărei căror cărui către da dacă dar datorită dată dau de deci deja deoarece departe deşi din dinaintea dintr- dintre doi doilea două drept după dă ea ei el ele eram este eu eşti face fata fi fie fiecare fii fim fiu fiţi frumos fără graţie halbă iar ieri la le li lor lui lângă lîngă mai mea mei mele mereu meu mi mie mine mult multă mulţi mulţumesc mâine mîine mă ne nevoie nici nicăieri nimeni nimeri nimic nişte noastre noastră noi noroc nostru nouă noştri nu opt ori oricare orice oricine oricum oricând oricât oricînd oricît oriunde patra patru patrulea pe pentru peste pic poate pot prea prima primul prin puţin puţina puţină până pînă rog sa sale sau se spate spre sub sunt suntem sunteţi sută sînt sîntem sînteţi să săi său ta tale te timp tine toate toată tot totuşi toţi trei treia treilea tu tăi tău un una unde undeva unei uneia unele uneori unii unor unora unu unui unuia unul vi voastre voastră voi vostru vouă voştri vreme vreo vreun vă zece zero zi zice îi îl îmi împotriva în înainte înaintea încotro încât încît între întrucât întrucît îţi ăla ălea ăsta ăstea ăştia şapte şase şi ştiu ţi ţie".split(" ")),e.Pipeline.registerFunction(e.ro.stopWordFilter,"stopWordFilter-ro")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.ru.min.js b/v2/assets/javascripts/lunr/min/lunr.ru.min.js new file mode 100644 index 000000000..186cc485c --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.ru.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Russian` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,n){"function"==typeof define&&define.amd?define(n):"object"==typeof exports?module.exports=n():n()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ru=function(){this.pipeline.reset(),this.pipeline.add(e.ru.trimmer,e.ru.stopWordFilter,e.ru.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ru.stemmer))},e.ru.wordCharacters="Ѐ-҄҇-ԯᴫᵸⷠ-ⷿꙀ-ꚟ︮︯",e.ru.trimmer=e.trimmerSupport.generateTrimmer(e.ru.wordCharacters),e.Pipeline.registerFunction(e.ru.trimmer,"trimmer-ru"),e.ru.stemmer=function(){var n=e.stemmerSupport.Among,r=e.stemmerSupport.SnowballProgram,t=new function(){function e(){for(;!W.in_grouping(S,1072,1103);){if(W.cursor>=W.limit)return!1;W.cursor++}return!0}function t(){for(;!W.out_grouping(S,1072,1103);){if(W.cursor>=W.limit)return!1;W.cursor++}return!0}function w(){b=W.limit,_=b,e()&&(b=W.cursor,t()&&e()&&t()&&(_=W.cursor))}function i(){return _<=W.cursor}function u(e,n){var r,t;if(W.ket=W.cursor,r=W.find_among_b(e,n)){switch(W.bra=W.cursor,r){case 1:if(t=W.limit-W.cursor,!W.eq_s_b(1,"а")&&(W.cursor=W.limit-t,!W.eq_s_b(1,"я")))return!1;case 2:W.slice_del()}return!0}return!1}function o(){return u(h,9)}function s(e,n){var r;return W.ket=W.cursor,!!(r=W.find_among_b(e,n))&&(W.bra=W.cursor,1==r&&W.slice_del(),!0)}function c(){return s(g,26)}function m(){return!!c()&&(u(C,8),!0)}function f(){return s(k,2)}function l(){return u(P,46)}function a(){s(v,36)}function p(){var e;W.ket=W.cursor,(e=W.find_among_b(F,2))&&(W.bra=W.cursor,i()&&1==e&&W.slice_del())}function d(){var e;if(W.ket=W.cursor,e=W.find_among_b(q,4))switch(W.bra=W.cursor,e){case 1:if(W.slice_del(),W.ket=W.cursor,!W.eq_s_b(1,"н"))break;W.bra=W.cursor;case 2:if(!W.eq_s_b(1,"н"))break;case 3:W.slice_del()}}var _,b,h=[new n("в",-1,1),new n("ив",0,2),new n("ыв",0,2),new n("вши",-1,1),new n("ивши",3,2),new n("ывши",3,2),new n("вшись",-1,1),new n("ившись",6,2),new n("ывшись",6,2)],g=[new n("ее",-1,1),new n("ие",-1,1),new n("ое",-1,1),new n("ые",-1,1),new n("ими",-1,1),new n("ыми",-1,1),new n("ей",-1,1),new n("ий",-1,1),new n("ой",-1,1),new n("ый",-1,1),new n("ем",-1,1),new n("им",-1,1),new n("ом",-1,1),new n("ым",-1,1),new n("его",-1,1),new n("ого",-1,1),new n("ему",-1,1),new n("ому",-1,1),new n("их",-1,1),new n("ых",-1,1),new n("ею",-1,1),new n("ою",-1,1),new n("ую",-1,1),new n("юю",-1,1),new n("ая",-1,1),new n("яя",-1,1)],C=[new n("ем",-1,1),new n("нн",-1,1),new n("вш",-1,1),new n("ивш",2,2),new n("ывш",2,2),new n("щ",-1,1),new n("ющ",5,1),new n("ующ",6,2)],k=[new n("сь",-1,1),new n("ся",-1,1)],P=[new n("ла",-1,1),new n("ила",0,2),new n("ыла",0,2),new n("на",-1,1),new n("ена",3,2),new n("ете",-1,1),new n("ите",-1,2),new n("йте",-1,1),new n("ейте",7,2),new n("уйте",7,2),new n("ли",-1,1),new n("или",10,2),new n("ыли",10,2),new n("й",-1,1),new n("ей",13,2),new n("уй",13,2),new n("л",-1,1),new n("ил",16,2),new n("ыл",16,2),new n("ем",-1,1),new n("им",-1,2),new n("ым",-1,2),new n("н",-1,1),new n("ен",22,2),new n("ло",-1,1),new n("ило",24,2),new n("ыло",24,2),new n("но",-1,1),new n("ено",27,2),new n("нно",27,1),new n("ет",-1,1),new n("ует",30,2),new n("ит",-1,2),new n("ыт",-1,2),new n("ют",-1,1),new n("уют",34,2),new n("ят",-1,2),new n("ны",-1,1),new n("ены",37,2),new n("ть",-1,1),new n("ить",39,2),new n("ыть",39,2),new n("ешь",-1,1),new n("ишь",-1,2),new n("ю",-1,2),new n("ую",44,2)],v=[new n("а",-1,1),new n("ев",-1,1),new n("ов",-1,1),new n("е",-1,1),new n("ие",3,1),new n("ье",3,1),new n("и",-1,1),new n("еи",6,1),new n("ии",6,1),new n("ами",6,1),new n("ями",6,1),new n("иями",10,1),new n("й",-1,1),new n("ей",12,1),new n("ией",13,1),new n("ий",12,1),new n("ой",12,1),new n("ам",-1,1),new n("ем",-1,1),new n("ием",18,1),new n("ом",-1,1),new n("ям",-1,1),new n("иям",21,1),new n("о",-1,1),new n("у",-1,1),new n("ах",-1,1),new n("ях",-1,1),new n("иях",26,1),new n("ы",-1,1),new n("ь",-1,1),new n("ю",-1,1),new n("ию",30,1),new n("ью",30,1),new n("я",-1,1),new n("ия",33,1),new n("ья",33,1)],F=[new n("ост",-1,1),new n("ость",-1,1)],q=[new n("ейше",-1,1),new n("н",-1,2),new n("ейш",-1,1),new n("ь",-1,3)],S=[33,65,8,232],W=new r;this.setCurrent=function(e){W.setCurrent(e)},this.getCurrent=function(){return W.getCurrent()},this.stem=function(){return w(),W.cursor=W.limit,!(W.cursor=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursors||e>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor>1),f=0,l=o0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.sv.min.js b/v2/assets/javascripts/lunr/min/lunr.sv.min.js new file mode 100644 index 000000000..3e5eb6400 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.sv.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Swedish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){function e(){var e,r=w.cursor+3;if(o=w.limit,0<=r||r<=w.limit){for(a=r;;){if(e=w.cursor,w.in_grouping(l,97,246)){w.cursor=e;break}if(w.cursor=e,w.cursor>=w.limit)return;w.cursor++}for(;!w.out_grouping(l,97,246);){if(w.cursor>=w.limit)return;w.cursor++}o=w.cursor,o=o&&(w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(u,37),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.in_grouping_b(d,98,121)&&w.slice_del()}}function i(){var e=w.limit_backward;w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.find_among_b(c,7)&&(w.cursor=w.limit,w.ket=w.cursor,w.cursor>w.limit_backward&&(w.bra=--w.cursor,w.slice_del())),w.limit_backward=e)}function s(){var e,r;if(w.cursor>=o){if(r=w.limit_backward,w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(m,5))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.slice_from("lös");break;case 3:w.slice_from("full")}w.limit_backward=r}}var a,o,u=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],c=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],l=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],d=[119,127,149],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,t(),w.cursor=w.limit,i(),w.cursor=w.limit,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}}(),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.tr.min.js b/v2/assets/javascripts/lunr/min/lunr.tr.min.js new file mode 100644 index 000000000..563f6ec1f --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.tr.min.js @@ -0,0 +1,18 @@ +/*! + * Lunr languages, `Turkish` language + * https://github.com/MihaiValentin/lunr-languages + * + * Copyright 2014, Mihai Valentin + * http://www.mozilla.org/MPL/ + */ +/*! + * based on + * Snowball JavaScript Library v0.3 + * http://code.google.com/p/urim/ + * http://snowball.tartarus.org/ + * + * Copyright 2010, Oleg Mazko + * http://www.mozilla.org/MPL/ + */ + +!function(r,i){"function"==typeof define&&define.amd?define(i):"object"==typeof exports?module.exports=i():i()(r.lunr)}(this,function(){return function(r){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");r.tr=function(){this.pipeline.reset(),this.pipeline.add(r.tr.trimmer,r.tr.stopWordFilter,r.tr.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(r.tr.stemmer))},r.tr.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",r.tr.trimmer=r.trimmerSupport.generateTrimmer(r.tr.wordCharacters),r.Pipeline.registerFunction(r.tr.trimmer,"trimmer-tr"),r.tr.stemmer=function(){var i=r.stemmerSupport.Among,e=r.stemmerSupport.SnowballProgram,n=new function(){function r(r,i,e){for(;;){var n=Dr.limit-Dr.cursor;if(Dr.in_grouping_b(r,i,e)){Dr.cursor=Dr.limit-n;break}if(Dr.cursor=Dr.limit-n,Dr.cursor<=Dr.limit_backward)return!1;Dr.cursor--}return!0}function n(){var i,e;i=Dr.limit-Dr.cursor,r(Wr,97,305);for(var n=0;nDr.limit_backward&&(Dr.cursor--,e=Dr.limit-Dr.cursor,i()))?(Dr.cursor=Dr.limit-e,!0):(Dr.cursor=Dr.limit-n,r()?(Dr.cursor=Dr.limit-n,!1):(Dr.cursor=Dr.limit-n,!(Dr.cursor<=Dr.limit_backward)&&(Dr.cursor--,!!i()&&(Dr.cursor=Dr.limit-n,!0))))}function u(r){return t(r,function(){return Dr.in_grouping_b(Wr,97,305)})}function o(){return u(function(){return Dr.eq_s_b(1,"n")})}function s(){return u(function(){return Dr.eq_s_b(1,"s")})}function c(){return u(function(){return Dr.eq_s_b(1,"y")})}function l(){return t(function(){return Dr.in_grouping_b(Lr,105,305)},function(){return Dr.out_grouping_b(Wr,97,305)})}function a(){return Dr.find_among_b(ur,10)&&l()}function m(){return n()&&Dr.in_grouping_b(Lr,105,305)&&s()}function d(){return Dr.find_among_b(or,2)}function f(){return n()&&Dr.in_grouping_b(Lr,105,305)&&c()}function b(){return n()&&Dr.find_among_b(sr,4)}function w(){return n()&&Dr.find_among_b(cr,4)&&o()}function _(){return n()&&Dr.find_among_b(lr,2)&&c()}function k(){return n()&&Dr.find_among_b(ar,2)}function p(){return n()&&Dr.find_among_b(mr,4)}function g(){return n()&&Dr.find_among_b(dr,2)}function y(){return n()&&Dr.find_among_b(fr,4)}function z(){return n()&&Dr.find_among_b(br,2)}function v(){return n()&&Dr.find_among_b(wr,2)&&c()}function h(){return Dr.eq_s_b(2,"ki")}function q(){return n()&&Dr.find_among_b(_r,2)&&o()}function C(){return n()&&Dr.find_among_b(kr,4)&&c()}function P(){return n()&&Dr.find_among_b(pr,4)}function F(){return n()&&Dr.find_among_b(gr,4)&&c()}function S(){return Dr.find_among_b(yr,4)}function W(){return n()&&Dr.find_among_b(zr,2)}function L(){return n()&&Dr.find_among_b(vr,4)}function x(){return n()&&Dr.find_among_b(hr,8)}function A(){return Dr.find_among_b(qr,2)}function E(){return n()&&Dr.find_among_b(Cr,32)&&c()}function j(){return Dr.find_among_b(Pr,8)&&c()}function T(){return n()&&Dr.find_among_b(Fr,4)&&c()}function Z(){return Dr.eq_s_b(3,"ken")&&c()}function B(){var r=Dr.limit-Dr.cursor;return!(T()||(Dr.cursor=Dr.limit-r,E()||(Dr.cursor=Dr.limit-r,j()||(Dr.cursor=Dr.limit-r,Z()))))}function D(){if(A()){var r=Dr.limit-Dr.cursor;if(S()||(Dr.cursor=Dr.limit-r,W()||(Dr.cursor=Dr.limit-r,C()||(Dr.cursor=Dr.limit-r,P()||(Dr.cursor=Dr.limit-r,F()||(Dr.cursor=Dr.limit-r))))),T())return!1}return!0}function G(){if(W()){Dr.bra=Dr.cursor,Dr.slice_del();var r=Dr.limit-Dr.cursor;return Dr.ket=Dr.cursor,x()||(Dr.cursor=Dr.limit-r,E()||(Dr.cursor=Dr.limit-r,j()||(Dr.cursor=Dr.limit-r,T()||(Dr.cursor=Dr.limit-r)))),nr=!1,!1}return!0}function H(){if(!L())return!0;var r=Dr.limit-Dr.cursor;return!E()&&(Dr.cursor=Dr.limit-r,!j())}function I(){var r,i=Dr.limit-Dr.cursor;return!(S()||(Dr.cursor=Dr.limit-i,F()||(Dr.cursor=Dr.limit-i,P()||(Dr.cursor=Dr.limit-i,C()))))||(Dr.bra=Dr.cursor,Dr.slice_del(),r=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,T()||(Dr.cursor=Dr.limit-r),!1)}function J(){var r,i=Dr.limit-Dr.cursor;if(Dr.ket=Dr.cursor,nr=!0,B()&&(Dr.cursor=Dr.limit-i,D()&&(Dr.cursor=Dr.limit-i,G()&&(Dr.cursor=Dr.limit-i,H()&&(Dr.cursor=Dr.limit-i,I()))))){if(Dr.cursor=Dr.limit-i,!x())return;Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,r=Dr.limit-Dr.cursor,S()||(Dr.cursor=Dr.limit-r,W()||(Dr.cursor=Dr.limit-r,C()||(Dr.cursor=Dr.limit-r,P()||(Dr.cursor=Dr.limit-r,F()||(Dr.cursor=Dr.limit-r))))),T()||(Dr.cursor=Dr.limit-r)}Dr.bra=Dr.cursor,Dr.slice_del()}function K(){var r,i,e,n;if(Dr.ket=Dr.cursor,h()){if(r=Dr.limit-Dr.cursor,p())return Dr.bra=Dr.cursor,Dr.slice_del(),i=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,W()?(Dr.bra=Dr.cursor,Dr.slice_del(),K()):(Dr.cursor=Dr.limit-i,a()&&(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()))),!0;if(Dr.cursor=Dr.limit-r,w()){if(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,e=Dr.limit-Dr.cursor,d())Dr.bra=Dr.cursor,Dr.slice_del();else{if(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,!a()&&(Dr.cursor=Dr.limit-e,!m()&&(Dr.cursor=Dr.limit-e,!K())))return!0;Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K())}return!0}if(Dr.cursor=Dr.limit-r,g()){if(n=Dr.limit-Dr.cursor,d())Dr.bra=Dr.cursor,Dr.slice_del();else if(Dr.cursor=Dr.limit-n,m())Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K());else if(Dr.cursor=Dr.limit-n,!K())return!1;return!0}}return!1}function M(r){if(Dr.ket=Dr.cursor,!g()&&(Dr.cursor=Dr.limit-r,!k()))return!1;var i=Dr.limit-Dr.cursor;if(d())Dr.bra=Dr.cursor,Dr.slice_del();else if(Dr.cursor=Dr.limit-i,m())Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K());else if(Dr.cursor=Dr.limit-i,!K())return!1;return!0}function N(r){if(Dr.ket=Dr.cursor,!z()&&(Dr.cursor=Dr.limit-r,!b()))return!1;var i=Dr.limit-Dr.cursor;return!(!m()&&(Dr.cursor=Dr.limit-i,!d()))&&(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()),!0)}function O(){var r,i=Dr.limit-Dr.cursor;return Dr.ket=Dr.cursor,!(!w()&&(Dr.cursor=Dr.limit-i,!v()))&&(Dr.bra=Dr.cursor,Dr.slice_del(),r=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,!(!W()||(Dr.bra=Dr.cursor,Dr.slice_del(),!K()))||(Dr.cursor=Dr.limit-r,Dr.ket=Dr.cursor,!(a()||(Dr.cursor=Dr.limit-r,m()||(Dr.cursor=Dr.limit-r,K())))||(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()),!0)))}function Q(){var r,i,e=Dr.limit-Dr.cursor;if(Dr.ket=Dr.cursor,!p()&&(Dr.cursor=Dr.limit-e,!f()&&(Dr.cursor=Dr.limit-e,!_())))return!1;if(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,r=Dr.limit-Dr.cursor,a())Dr.bra=Dr.cursor,Dr.slice_del(),i=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,W()||(Dr.cursor=Dr.limit-i);else if(Dr.cursor=Dr.limit-r,!W())return!0;return Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,K(),!0}function R(){var r,i,e=Dr.limit-Dr.cursor;if(Dr.ket=Dr.cursor,W())return Dr.bra=Dr.cursor,Dr.slice_del(),void K();if(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,q())if(Dr.bra=Dr.cursor,Dr.slice_del(),r=Dr.limit-Dr.cursor,Dr.ket=Dr.cursor,d())Dr.bra=Dr.cursor,Dr.slice_del();else{if(Dr.cursor=Dr.limit-r,Dr.ket=Dr.cursor,!a()&&(Dr.cursor=Dr.limit-r,!m())){if(Dr.cursor=Dr.limit-r,Dr.ket=Dr.cursor,!W())return;if(Dr.bra=Dr.cursor,Dr.slice_del(),!K())return}Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K())}else if(Dr.cursor=Dr.limit-e,!M(e)&&(Dr.cursor=Dr.limit-e,!N(e))){if(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,y())return Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,i=Dr.limit-Dr.cursor,void(a()?(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K())):(Dr.cursor=Dr.limit-i,W()?(Dr.bra=Dr.cursor,Dr.slice_del(),K()):(Dr.cursor=Dr.limit-i,K())));if(Dr.cursor=Dr.limit-e,!O()){if(Dr.cursor=Dr.limit-e,d())return Dr.bra=Dr.cursor,void Dr.slice_del();Dr.cursor=Dr.limit-e,K()||(Dr.cursor=Dr.limit-e,Q()||(Dr.cursor=Dr.limit-e,Dr.ket=Dr.cursor,(a()||(Dr.cursor=Dr.limit-e,m()))&&(Dr.bra=Dr.cursor,Dr.slice_del(),Dr.ket=Dr.cursor,W()&&(Dr.bra=Dr.cursor,Dr.slice_del(),K()))))}}}function U(){var r;if(Dr.ket=Dr.cursor,r=Dr.find_among_b(Sr,4))switch(Dr.bra=Dr.cursor,r){case 1:Dr.slice_from("p");break;case 2:Dr.slice_from("ç");break;case 3:Dr.slice_from("t");break;case 4:Dr.slice_from("k")}}function V(){for(;;){var r=Dr.limit-Dr.cursor;if(Dr.in_grouping_b(Wr,97,305)){Dr.cursor=Dr.limit-r;break}if(Dr.cursor=Dr.limit-r,Dr.cursor<=Dr.limit_backward)return!1;Dr.cursor--}return!0}function X(r,i,e){if(Dr.cursor=Dr.limit-r,V()){var n=Dr.limit-Dr.cursor;if(!Dr.eq_s_b(1,i)&&(Dr.cursor=Dr.limit-n,!Dr.eq_s_b(1,e)))return!0;Dr.cursor=Dr.limit-r;var t=Dr.cursor;return Dr.insert(Dr.cursor,Dr.cursor,e),Dr.cursor=t,!1}return!0}function Y(){var r=Dr.limit-Dr.cursor;(Dr.eq_s_b(1,"d")||(Dr.cursor=Dr.limit-r,Dr.eq_s_b(1,"g")))&&X(r,"a","ı")&&X(r,"e","i")&&X(r,"o","u")&&X(r,"ö","ü")}function $(){for(var r,i=Dr.cursor,e=2;;){for(r=Dr.cursor;!Dr.in_grouping(Wr,97,305);){if(Dr.cursor>=Dr.limit)return Dr.cursor=r,!(e>0)&&(Dr.cursor=i,!0);Dr.cursor++}e--}}function rr(r,i,e){for(;!Dr.eq_s(i,e);){if(Dr.cursor>=Dr.limit)return!0;Dr.cursor++}return(tr=i)!=Dr.limit||(Dr.cursor=r,!1)}function ir(){var r=Dr.cursor;return!rr(r,2,"ad")||(Dr.cursor=r,!rr(r,5,"soyad"))}function er(){var r=Dr.cursor;return!ir()&&(Dr.limit_backward=r,Dr.cursor=Dr.limit,Y(),Dr.cursor=Dr.limit,U(),!0)}var nr,tr,ur=[new i("m",-1,-1),new i("n",-1,-1),new i("miz",-1,-1),new i("niz",-1,-1),new i("muz",-1,-1),new i("nuz",-1,-1),new i("müz",-1,-1),new i("nüz",-1,-1),new i("mız",-1,-1),new i("nız",-1,-1)],or=[new i("leri",-1,-1),new i("ları",-1,-1)],sr=[new i("ni",-1,-1),new i("nu",-1,-1),new i("nü",-1,-1),new i("nı",-1,-1)],cr=[new i("in",-1,-1),new i("un",-1,-1),new i("ün",-1,-1),new i("ın",-1,-1)],lr=[new i("a",-1,-1),new i("e",-1,-1)],ar=[new i("na",-1,-1),new i("ne",-1,-1)],mr=[new i("da",-1,-1),new i("ta",-1,-1),new i("de",-1,-1),new i("te",-1,-1)],dr=[new i("nda",-1,-1),new i("nde",-1,-1)],fr=[new i("dan",-1,-1),new i("tan",-1,-1),new i("den",-1,-1),new i("ten",-1,-1)],br=[new i("ndan",-1,-1),new i("nden",-1,-1)],wr=[new i("la",-1,-1),new i("le",-1,-1)],_r=[new i("ca",-1,-1),new i("ce",-1,-1)],kr=[new i("im",-1,-1),new i("um",-1,-1),new i("üm",-1,-1),new i("ım",-1,-1)],pr=[new i("sin",-1,-1),new i("sun",-1,-1),new i("sün",-1,-1),new i("sın",-1,-1)],gr=[new i("iz",-1,-1),new i("uz",-1,-1),new i("üz",-1,-1),new i("ız",-1,-1)],yr=[new i("siniz",-1,-1),new i("sunuz",-1,-1),new i("sünüz",-1,-1),new i("sınız",-1,-1)],zr=[new i("lar",-1,-1),new i("ler",-1,-1)],vr=[new i("niz",-1,-1),new i("nuz",-1,-1),new i("nüz",-1,-1),new i("nız",-1,-1)],hr=[new i("dir",-1,-1),new i("tir",-1,-1),new i("dur",-1,-1),new i("tur",-1,-1),new i("dür",-1,-1),new i("tür",-1,-1),new i("dır",-1,-1),new i("tır",-1,-1)],qr=[new i("casına",-1,-1),new i("cesine",-1,-1)],Cr=[new i("di",-1,-1),new i("ti",-1,-1),new i("dik",-1,-1),new i("tik",-1,-1),new i("duk",-1,-1),new i("tuk",-1,-1),new i("dük",-1,-1),new i("tük",-1,-1),new i("dık",-1,-1),new i("tık",-1,-1),new i("dim",-1,-1),new i("tim",-1,-1),new i("dum",-1,-1),new i("tum",-1,-1),new i("düm",-1,-1),new i("tüm",-1,-1),new i("dım",-1,-1),new i("tım",-1,-1),new i("din",-1,-1),new i("tin",-1,-1),new i("dun",-1,-1),new i("tun",-1,-1),new i("dün",-1,-1),new i("tün",-1,-1),new i("dın",-1,-1),new i("tın",-1,-1),new i("du",-1,-1),new i("tu",-1,-1),new i("dü",-1,-1),new i("tü",-1,-1),new i("dı",-1,-1),new i("tı",-1,-1)],Pr=[new i("sa",-1,-1),new i("se",-1,-1),new i("sak",-1,-1),new i("sek",-1,-1),new i("sam",-1,-1),new i("sem",-1,-1),new i("san",-1,-1),new i("sen",-1,-1)],Fr=[new i("miş",-1,-1),new i("muş",-1,-1),new i("müş",-1,-1),new i("mış",-1,-1)],Sr=[new i("b",-1,1),new i("c",-1,2),new i("d",-1,3),new i("ğ",-1,4)],Wr=[17,65,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,32,8,0,0,0,0,0,0,1],Lr=[1,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,0,0,0,0,0,0,1],xr=[1,64,16,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],Ar=[17,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,130],Er=[1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],jr=[17],Tr=[65],Zr=[65],Br=[["a",xr,97,305],["e",Ar,101,252],["ı",Er,97,305],["i",jr,101,105],["o",Tr,111,117],["ö",Zr,246,252],["u",Tr,111,117]],Dr=new e;this.setCurrent=function(r){Dr.setCurrent(r)},this.getCurrent=function(){return Dr.getCurrent()},this.stem=function(){return!!($()&&(Dr.limit_backward=Dr.cursor,Dr.cursor=Dr.limit,J(),Dr.cursor=Dr.limit,nr&&(R(),Dr.cursor=Dr.limit_backward,er())))}};return function(r){return"function"==typeof r.update?r.update(function(r){return n.setCurrent(r),n.stem(),n.getCurrent()}):(n.setCurrent(r),n.stem(),n.getCurrent())}}(),r.Pipeline.registerFunction(r.tr.stemmer,"stemmer-tr"),r.tr.stopWordFilter=r.generateStopWordFilter("acaba altmış altı ama ancak arada aslında ayrıca bana bazı belki ben benden beni benim beri beş bile bin bir biri birkaç birkez birçok birşey birşeyi biz bizden bize bizi bizim bu buna bunda bundan bunlar bunları bunların bunu bunun burada böyle böylece da daha dahi de defa değil diye diğer doksan dokuz dolayı dolayısıyla dört edecek eden ederek edilecek ediliyor edilmesi ediyor elli en etmesi etti ettiği ettiğini eğer gibi göre halen hangi hatta hem henüz hep hepsi her herhangi herkesin hiç hiçbir iki ile ilgili ise itibaren itibariyle için işte kadar karşın katrilyon kendi kendilerine kendini kendisi kendisine kendisini kez ki kim kimden kime kimi kimse kırk milyar milyon mu mü mı nasıl ne neden nedenle nerde nerede nereye niye niçin o olan olarak oldu olduklarını olduğu olduğunu olmadı olmadığı olmak olması olmayan olmaz olsa olsun olup olur olursa oluyor on ona ondan onlar onlardan onları onların onu onun otuz oysa pek rağmen sadece sanki sekiz seksen sen senden seni senin siz sizden sizi sizin tarafından trilyon tüm var vardı ve veya ya yani yapacak yapmak yaptı yaptıkları yaptığı yaptığını yapılan yapılması yapıyor yedi yerine yetmiş yine yirmi yoksa yüz zaten çok çünkü öyle üzere üç şey şeyden şeyi şeyler şu şuna şunda şundan şunları şunu şöyle".split(" ")),r.Pipeline.registerFunction(r.tr.stopWordFilter,"stopWordFilter-tr")}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/min/lunr.vi.min.js b/v2/assets/javascripts/lunr/min/lunr.vi.min.js new file mode 100644 index 000000000..22aed28c4 --- /dev/null +++ b/v2/assets/javascripts/lunr/min/lunr.vi.min.js @@ -0,0 +1 @@ +!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.vi=function(){this.pipeline.reset(),this.pipeline.add(e.vi.stopWordFilter,e.vi.trimmer)},e.vi.wordCharacters="[A-Za-ẓ̀͐́͑̉̃̓ÂâÊêÔôĂ-ăĐ-đƠ-ơƯ-ư]",e.vi.trimmer=e.trimmerSupport.generateTrimmer(e.vi.wordCharacters),e.Pipeline.registerFunction(e.vi.trimmer,"trimmer-vi"),e.vi.stopWordFilter=e.generateStopWordFilter("là cái nhưng mà".split(" "))}}); \ No newline at end of file diff --git a/v2/assets/javascripts/lunr/tinyseg.min.js b/v2/assets/javascripts/lunr/tinyseg.min.js new file mode 100644 index 000000000..302befbb3 --- /dev/null +++ b/v2/assets/javascripts/lunr/tinyseg.min.js @@ -0,0 +1 @@ +!function(_,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(_.lunr)}(this,(function(){return function(_){function t(){var _={"[一二三四五六七八九十百千万億兆]":"M","[一-龠々〆ヵヶ]":"H","[ぁ-ん]":"I","[ァ-ヴーア-ン゙ー]":"K","[a-zA-Za-zA-Z]":"A","[0-90-9]":"N"};for(var t in this.chartype_=[],_){var H=new RegExp(t);this.chartype_.push([H,_[t]])}return this.BIAS__=-332,this.BC1__={HH:6,II:2461,KH:406,OH:-1378},this.BC2__={AA:-3267,AI:2744,AN:-878,HH:-4070,HM:-1711,HN:4012,HO:3761,IA:1327,IH:-1184,II:-1332,IK:1721,IO:5492,KI:3831,KK:-8741,MH:-3132,MK:3334,OO:-2920},this.BC3__={HH:996,HI:626,HK:-721,HN:-1307,HO:-836,IH:-301,KK:2762,MK:1079,MM:4034,OA:-1652,OH:266},this.BP1__={BB:295,OB:304,OO:-125,UB:352},this.BP2__={BO:60,OO:-1762},this.BQ1__={BHH:1150,BHM:1521,BII:-1158,BIM:886,BMH:1208,BNH:449,BOH:-91,BOO:-2597,OHI:451,OIH:-296,OKA:1851,OKH:-1020,OKK:904,OOO:2965},this.BQ2__={BHH:118,BHI:-1159,BHM:466,BIH:-919,BKK:-1720,BKO:864,OHH:-1139,OHM:-181,OIH:153,UHI:-1146},this.BQ3__={BHH:-792,BHI:2664,BII:-299,BKI:419,BMH:937,BMM:8335,BNN:998,BOH:775,OHH:2174,OHM:439,OII:280,OKH:1798,OKI:-793,OKO:-2242,OMH:-2402,OOO:11699},this.BQ4__={BHH:-3895,BIH:3761,BII:-4654,BIK:1348,BKK:-1806,BMI:-3385,BOO:-12396,OAH:926,OHH:266,OHK:-2036,ONN:-973},this.BW1__={",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682},this.BW2__={"..":-11822,11:-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669},this.BW3__={"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1e3,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990},this.TC1__={AAA:1093,HHH:1029,HHM:580,HII:998,HOH:-390,HOM:-331,IHI:1169,IOH:-142,IOI:-1015,IOM:467,MMH:187,OOI:-1832},this.TC2__={HHO:2088,HII:-1023,HMM:-1154,IHI:-1965,KKH:703,OII:-2649},this.TC3__={AAA:-294,HHH:346,HHI:-341,HII:-1088,HIK:731,HOH:-1486,IHH:128,IHI:-3041,IHO:-1935,IIH:-825,IIM:-1035,IOI:-542,KHH:-1216,KKA:491,KKH:-1217,KOK:-1009,MHH:-2694,MHM:-457,MHO:123,MMH:-471,NNH:-1689,NNO:662,OHO:-3393},this.TC4__={HHH:-203,HHI:1344,HHK:365,HHM:-122,HHN:182,HHO:669,HIH:804,HII:679,HOH:446,IHH:695,IHO:-2324,IIH:321,III:1497,IIO:656,IOO:54,KAK:4845,KKA:3386,KKK:3065,MHH:-405,MHI:201,MMH:-241,MMM:661,MOM:841},this.TQ1__={BHHH:-227,BHHI:316,BHIH:-132,BIHH:60,BIII:1595,BNHH:-744,BOHH:225,BOOO:-908,OAKK:482,OHHH:281,OHIH:249,OIHI:200,OIIH:-68},this.TQ2__={BIHH:-1401,BIII:-1033,BKAK:-543,BOOO:-5591},this.TQ3__={BHHH:478,BHHM:-1073,BHIH:222,BHII:-504,BIIH:-116,BIII:-105,BMHI:-863,BMHM:-464,BOMH:620,OHHH:346,OHHI:1729,OHII:997,OHMH:481,OIHH:623,OIIH:1344,OKAK:2792,OKHH:587,OKKA:679,OOHH:110,OOII:-685},this.TQ4__={BHHH:-721,BHHM:-3604,BHII:-966,BIIH:-607,BIII:-2181,OAAA:-2763,OAKK:180,OHHH:-294,OHHI:2446,OHHO:480,OHIH:-1573,OIHH:1935,OIHI:-493,OIIH:626,OIII:-4007,OKAK:-8156},this.TW1__={"につい":-4681,"東京都":2026},this.TW2__={"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216},this.TW3__={"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287},this.TW4__={"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865},this.UC1__={A:484,K:93,M:645,O:-505},this.UC2__={A:819,H:1059,I:409,M:3987,N:5775,O:646},this.UC3__={A:-1370,I:2311},this.UC4__={A:-2643,H:1809,I:-1032,K:-3450,M:3565,N:3876,O:6646},this.UC5__={H:313,I:-1238,K:-799,M:539,O:-831},this.UC6__={H:-506,I:-253,K:87,M:247,O:-387},this.UP1__={O:-214},this.UP2__={B:69,O:935},this.UP3__={B:189},this.UQ1__={BH:21,BI:-12,BK:-99,BN:142,BO:-56,OH:-95,OI:477,OK:410,OO:-2422},this.UQ2__={BH:216,BI:113,OK:1759},this.UQ3__={BA:-479,BH:42,BI:1913,BK:-7198,BM:3160,BN:6427,BO:14761,OI:-827,ON:-3212},this.UW1__={",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135},this.UW2__={",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568},this.UW3__={",":4889,1:-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278},this.UW4__={",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1e3,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637},this.UW5__={",":465,".":-299,1:-514,E2:-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343},this.UW6__={",":227,".":808,1:-270,E1:306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496},this}t.prototype.ctype_=function(_){for(var t in this.chartype_)if(_.match(this.chartype_[t][0]))return this.chartype_[t][1];return"O"},t.prototype.ts_=function(_){return _||0},t.prototype.segment=function(_){if(null==_||null==_||""==_)return[];var t=[],H=["B3","B2","B1"],s=["O","O","O"],h=_.split("");for(K=0;K0&&(t.push(i),i="",N="B"),I=O,O=B,B=N,i+=H[K]}return t.push(i),t},_.TinySegmenter=t}})); \ No newline at end of file diff --git a/v2/assets/javascripts/vendor.77e55a48.min.js b/v2/assets/javascripts/vendor.77e55a48.min.js new file mode 100644 index 000000000..4b293bef1 --- /dev/null +++ b/v2/assets/javascripts/vendor.77e55a48.min.js @@ -0,0 +1,30 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[1],[,function(t,e,n){"use strict";function r(t){return"function"==typeof(null==t?void 0:t.lift)}function i(t){return e=>{if(r(e))return e.lift((function(e){try{return t(e,this)}catch(t){this.error(t)}}));throw new TypeError("Unable to lift unknown Observable type")}}n.d(e,"a",(function(){return r})),n.d(e,"b",(function(){return i}))},,function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(25);class i extends r.b{constructor(t,e,n,r,i){super(t),this.onUnsubscribe=i,e&&(this._next=function(t){try{e(t)}catch(t){this.error(t)}}),n&&(this._error=function(t){try{n(t)}catch(t){this.destination.error(t)}this.unsubscribe()}),r&&(this._complete=function(){try{r()}catch(t){this.destination.error(t)}this.unsubscribe()})}unsubscribe(){var t;!this.closed&&(null===(t=this.onUnsubscribe)||void 0===t||t.call(this)),super.unsubscribe()}}},,,function(t,e,n){"use strict";n.d(e,"a",(function(){return a}));var r=n(25),i=n(7),o=n(13),s=n(33),c=n(17),u=n(29);class a{constructor(t){t&&(this._subscribe=t)}lift(t){const e=new a;return e.source=this,e.operator=t,e}subscribe(t,e,n){const o=(s=t)&&s instanceof r.b||function(t){return t&&"function"==typeof t.next&&"function"==typeof t.error&&"function"==typeof t.complete}(s)&&Object(i.c)(s)?t:new r.a(t,e,n);var s;const{operator:u,source:a}=this;return o.add(u?u.call(o,a):a||c.a.useDeprecatedSynchronousErrorHandling?this._subscribe(o):this._trySubscribe(o)),o}_trySubscribe(t){try{return this._subscribe(t)}catch(e){if(c.a.useDeprecatedSynchronousErrorHandling)throw e;!function(t){for(;t;){const{closed:e,destination:n,isStopped:i}=t;if(e||i)return!1;t=n&&n instanceof r.b?n:null}return!0}(t)?Object(u.a)(e):t.error(e)}}forEach(t,e){return new(e=l(e))((e,n)=>{let r;r=this.subscribe(e=>{try{t(e)}catch(t){n(t),null==r||r.unsubscribe()}},n,e)})}_subscribe(t){var e;return null===(e=this.source)||void 0===e?void 0:e.subscribe(t)}[o.a](){return this}pipe(...t){return t.length?Object(s.b)(t)(this):this}toPromise(t){return new(t=l(t))((t,e)=>{let n;this.subscribe(t=>n=t,t=>e(t),()=>t(n))})}}function l(t){var e;return null!==(e=null!=t?t:c.a.Promise)&&void 0!==e?e:Promise}a.create=t=>new a(t)},function(t,e,n){"use strict";n.d(e,"b",(function(){return u})),n.d(e,"a",(function(){return a})),n.d(e,"c",(function(){return l}));var r=n(21),i=n(37);const o=Object(i.a)(t=>function(e){t(this),this.message=e?`${e.length} errors occurred during unsubscription:\n${e.map((t,e)=>`${e+1}) ${t.toString()}`).join("\n ")}`:"",this.name="UnsubscriptionError",this.errors=e});var s,c=n(16);class u{constructor(t){this.initialTeardown=t,this.closed=!1,this._parentage=null,this._teardowns=null}unsubscribe(){let t;if(!this.closed){this.closed=!0;const{_parentage:e}=this;if(Array.isArray(e))for(const t of e)t.remove(this);else null==e||e.remove(this);const{initialTeardown:n}=this;if(Object(r.a)(n))try{n()}catch(e){t=e instanceof o?e.errors:[e]}const{_teardowns:i}=this;if(i){this._teardowns=null;for(const e of i)try{f(e)}catch(e){t=null!=t?t:[],e instanceof o?t=[...t,...e.errors]:t.push(e)}}if(t)throw new o(t)}}add(t){var e;if(t&&t!==this)if(this.closed)f(t);else{if(t instanceof u){if(t.closed||t._hasParent(this))return;t._addParent(this)}(this._teardowns=null!==(e=this._teardowns)&&void 0!==e?e:[]).push(t)}}_hasParent(t){const{_parentage:e}=this;return e===t||Array.isArray(e)&&e.includes(t)}_addParent(t){const{_parentage:e}=this;this._parentage=Array.isArray(e)?(e.push(t),e):e?[e,t]:t}_removeParent(t){const{_parentage:e}=this;e===t?this._parentage=null:Array.isArray(e)&&Object(c.a)(e,t)}remove(t){const{_teardowns:e}=this;e&&Object(c.a)(e,t),t instanceof u&&t._removeParent(this)}}u.EMPTY=((s=new u).closed=!0,s);const a=u.EMPTY;function l(t){return t instanceof u||t&&"closed"in t&&"function"==typeof t.remove&&"function"==typeof t.add&&"function"==typeof t.unsubscribe}function f(t){"function"==typeof t?t():t.unsubscribe()}},,function(t,e,n){"use strict";n.d(e,"a",(function(){return m}));var r=n(40),i=n(29);const o="function"==typeof Symbol&&Symbol.iterator?Symbol.iterator:"@@iterator";var s=n(13);var c=n(30);function u(t){return!!t&&"function"!=typeof t.subscribe&&"function"==typeof t.then}function a(t,e,n,r){return new(n||(n=Promise))((function(i,o){function s(t){try{u(r.next(t))}catch(t){o(t)}}function c(t){try{u(r.throw(t))}catch(t){o(t)}}function u(t){var e;t.done?i(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(s,c)}u((r=r.apply(t,e||[])).next())}))}Object.create;function l(t){var e="function"==typeof Symbol&&Symbol.iterator,n=e&&t[e],r=0;if(n)return n.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&r>=t.length&&(t=void 0),{value:t&&t[r++],done:!t}}};throw new TypeError(e?"Object is not iterable.":"Symbol.iterator is not defined.")}function f(t){if(!Symbol.asyncIterator)throw new TypeError("Symbol.asyncIterator is not defined.");var e,n=t[Symbol.asyncIterator];return n?n.call(t):(t=l(t),e={},r("next"),r("throw"),r("return"),e[Symbol.asyncIterator]=function(){return this},e);function r(n){e[n]=t[n]&&function(e){return new Promise((function(r,i){(function(t,e,n,r){Promise.resolve(r).then((function(e){t({value:e,done:n})}),e)})(r,i,(e=t[n](e)).done,e.value)}))}}}Object.create;function d(t){return e=>{(function(t,e){var n,r,i,o;return a(this,void 0,void 0,(function*(){try{for(n=f(t);!(r=yield n.next()).done;){const t=r.value;e.next(t)}}catch(t){i={error:t}}finally{try{r&&!r.done&&(o=n.return)&&(yield o.call(n))}finally{if(i)throw i.error}}e.complete()}))})(t,e).catch(t=>e.error(t))}}var h=n(6),b=n(7);var p=n(31);function v(t,e){if(null!=t){if(function(t){return t&&"function"==typeof t[s.a]}(t))return function(t,e){return new h.a(n=>{const r=new b.b;return r.add(e.schedule(()=>{const i=t[s.a]();r.add(i.subscribe({next(t){r.add(e.schedule(()=>n.next(t)))},error(t){r.add(e.schedule(()=>n.error(t)))},complete(){r.add(e.schedule(()=>n.complete()))}}))})),r})}(t,e);if(u(t))return function(t,e){return new h.a(n=>{const r=new b.b;return r.add(e.schedule(()=>t.then(t=>{r.add(e.schedule(()=>{n.next(t),r.add(e.schedule(()=>n.complete()))}))},t=>{r.add(e.schedule(()=>n.error(t)))}))),r})}(t,e);if(Object(c.a)(t))return Object(p.a)(t,e);if(function(t){return t&&"function"==typeof t[o]}(t)||"string"==typeof t)return function(t,e){if(!t)throw new Error("Iterable cannot be null");return new h.a(n=>{const r=new b.b;let i;return r.add(()=>{i&&"function"==typeof i.return&&i.return()}),r.add(e.schedule(()=>{i=t[o](),r.add(e.schedule((function(){if(n.closed)return;let t,e;try{const n=i.next();t=n.value,e=n.done}catch(t){return void n.error(t)}e?n.complete():(n.next(t),this.schedule())})))})),r})}(t,e);if(Symbol&&Symbol.asyncIterator&&"function"==typeof t[Symbol.asyncIterator])return function(t,e){if(!t)throw new Error("Iterable cannot be null");return new h.a(n=>{const r=new b.b;return r.add(e.schedule(()=>{const i=t[Symbol.asyncIterator]();r.add(e.schedule((function(){i.next().then(t=>{t.done?n.complete():(n.next(t.value),this.schedule())})})))})),r})}(t,e)}throw new TypeError((null!==t&&typeof t||t)+" is not observable")}function m(t,e){return e?v(t,e):t instanceof h.a?t:new h.a(function(t){if(t&&"function"==typeof t[s.a])return l=t,t=>{const e=l[s.a]();if("function"!=typeof e.subscribe)throw new TypeError("Provided object does not correctly implement Symbol.observable");return e.subscribe(t)};if(Object(c.a)(t))return Object(r.a)(t);if(u(t))return a=t,t=>(a.then(e=>{t.closed||(t.next(e),t.complete())},e=>t.error(e)).then(null,i.a),t);if(t&&"function"==typeof t[o])return n=t,t=>{const e=n[o]();for(;;){let n;try{n=e.next()}catch(e){return void t.error(e)}if(n.done){t.complete();break}if(t.next(n.value),t.closed)break}return"function"==typeof e.return&&t.add(()=>{e.return&&e.return()}),t};if(Symbol&&Symbol.asyncIterator&&t&&"function"==typeof t[Symbol.asyncIterator])return d(t);{const n=null!==(e=t)&&"object"==typeof e?"an invalid object":`'${t}'`;throw new TypeError(`You provided ${n} where a stream was expected. You can provide an Observable, Promise, Array, AsyncIterable, or Iterable.`)}var e; +/*! ***************************************************************************** +Copyright (c) Microsoft Corporation. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. +***************************************************************************** */var n;var a;var l}(t))}},,function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t,e){return Object(r.b)((n,r)=>{let o=0;n.subscribe(new i.a(r,n=>{r.next(t.call(e,n,o++))}))})}},function(t,e,n){"use strict";function r(t){return t}n.d(e,"a",(function(){return r}))},function(t,e,n){"use strict";n.d(e,"a",(function(){return r}));const r="function"==typeof Symbol&&Symbol.observable||"@@observable"},function(t,e,n){"use strict";function r(t){return t&&"function"==typeof t.schedule}n.d(e,"a",(function(){return r}))},,function(t,e,n){"use strict";function r(t,e){if(t){const n=t.indexOf(e);0<=n&&t.splice(n,1)}}n.d(e,"a",(function(){return r}))},function(t,e,n){"use strict";n.d(e,"a",(function(){return r}));const r={onUnhandledError:null,Promise:void 0,useDeprecatedSynchronousErrorHandling:!1,useDeprecatedNextContext:!1}},function(t,e,n){"use strict";function r(){}n.d(e,"a",(function(){return r}))},,function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(9),i=n(1),o=n(3);function s(t,e){return Object(i.b)((n,i)=>{let s=null,c=0,u=!1;const a=()=>u&&!s&&i.complete();n.subscribe(new o.a(i,n=>{null==s||s.unsubscribe();let u=0,l=c++;Object(r.a)(t(n,l)).subscribe(s=new o.a(i,t=>i.next(e?e(n,t,l,u++):t),void 0,()=>{s=null,a()}))},void 0,()=>{u=!0,a()}))})}},function(t,e,n){"use strict";function r(t){return"function"==typeof t}n.d(e,"a",(function(){return r}))},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(6),i=n(40),o=n(31);function s(t,e){return e?Object(o.a)(t,e):new r.a(Object(i.a)(t))}},,,function(t,e,n){"use strict";n.d(e,"b",(function(){return u})),n.d(e,"a",(function(){return a}));var r=n(21),i=n(7),o=n(17),s=n(29),c=n(18);class u extends i.b{constructor(t){super(),this.isStopped=!1,t?(this.destination=t,Object(i.c)(t)&&t.add(this)):this.destination=f}static create(t,e,n){return new a(t,e,n)}next(t){this.isStopped||this._next(t)}error(t){this.isStopped||(this.isStopped=!0,this._error(t))}complete(){this.isStopped||(this.isStopped=!0,this._complete())}unsubscribe(){this.closed||(this.isStopped=!0,super.unsubscribe())}_next(t){this.destination.next(t)}_error(t){this.destination.error(t),this.unsubscribe()}_complete(){this.destination.complete(),this.unsubscribe()}}class a extends u{constructor(t,e,n){if(super(),this.destination=f,(t||e||n)&&t!==f){let i;if(Object(r.a)(t))i=t;else if(t){let r;({next:i,error:e,complete:n}=t),this&&o.a.useDeprecatedNextContext?(r=Object.create(t),r.unsubscribe=()=>this.unsubscribe()):r=t,i=null==i?void 0:i.bind(r),e=null==e?void 0:e.bind(r),n=null==n?void 0:n.bind(r)}this.destination={next:i||c.a,error:e||l,complete:n||c.a}}}}function l(t){if(o.a.useDeprecatedSynchronousErrorHandling)throw t;Object(s.a)(t)}const f={closed:!0,next:c.a,error:l,complete:c.a}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(6);const i=new r.a(t=>t.complete())},,function(t,e,n){"use strict";n.d(e,"a",(function(){return c}));var r=n(11),i=n(9),o=n(1),s=n(3);function c(t,e,n=1/0){return"function"==typeof e?o=>o.pipe(c((n,o)=>Object(i.a)(t(n,o)).pipe(Object(r.a)((t,r)=>e(n,t,o,r))),n)):("number"==typeof e&&(n=e),Object(o.b)((e,r)=>{let o=!1,c=0,u=0,a=[];const l=()=>o&&!c&&r.complete(),f=e=>{c++,r.add(Object(i.a)(t(e,u++)).subscribe(new s.a(r,t=>r.next(t),void 0,()=>{c--,a.length&&(()=>{for(;c0;)f(a.shift())})(),l()})))};let d;return d=e.subscribe(new s.a(r,t=>c{o=!0,l(),null==d||d.unsubscribe()})),()=>{a=null}}))}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(17);function i(t){setTimeout(()=>{const{onUnhandledError:e}=r.a;if(!e)throw t;e(t)})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return r}));const r=t=>t&&"number"==typeof t.length&&"function"!=typeof t},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(6),i=n(7);function o(t,e){return new r.a(n=>{const r=new i.b;let o=0;return r.add(e.schedule((function(){o!==t.length?(n.next(t[o++]),n.closed||r.add(this.schedule())):n.complete()}))),r})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return u}));var r=n(6),i=n(7),o=n(37);const s=Object(o.a)(t=>function(){t(this),this.message="object unsubscribed"});var c=n(16);class u extends r.a{constructor(){super(),this.observers=[],this.closed=!1,this.isStopped=!1,this.hasError=!1,this.thrownError=null}lift(t){const e=new a(this,this);return e.operator=t,e}_throwIfClosed(){if(this.closed)throw new s}next(t){if(this._throwIfClosed(),!this.isStopped){const e=this.observers.slice();for(const n of e)n.next(t)}}error(t){if(this._throwIfClosed(),!this.isStopped){this.hasError=this.isStopped=!0,this.thrownError=t;const{observers:e}=this;for(;e.length;)e.shift().error(t)}}complete(){if(this._throwIfClosed(),!this.isStopped){this.isStopped=!0;const{observers:t}=this;for(;t.length;)t.shift().complete()}}unsubscribe(){this.isStopped=this.closed=!0,this.observers=null}_trySubscribe(t){return this._throwIfClosed(),super._trySubscribe(t)}_subscribe(t){return this._throwIfClosed(),this._checkFinalizedStatuses(t),this._innerSubscribe(t)}_innerSubscribe(t){const{hasError:e,isStopped:n,observers:r}=this;return e||n?i.a:(r.push(t),new i.b(()=>Object(c.a)(this.observers,t)))}_checkFinalizedStatuses(t){const{hasError:e,thrownError:n,isStopped:r}=this;e?t.error(n):r&&t.complete()}asObservable(){const t=new r.a;return t.source=this,t}}u.create=(t,e)=>new a(t,e);class a extends u{constructor(t,e){super(),this.destination=t,this.source=e}next(t){var e,n;null===(n=null===(e=this.destination)||void 0===e?void 0:e.next)||void 0===n||n.call(e,t)}error(t){var e,n;null===(n=null===(e=this.destination)||void 0===e?void 0:e.error)||void 0===n||n.call(e,t)}complete(){var t,e;null===(e=null===(t=this.destination)||void 0===t?void 0:t.complete)||void 0===e||e.call(t)}_subscribe(t){var e,n;return null!==(n=null===(e=this.source)||void 0===e?void 0:e.subscribe(t))&&void 0!==n?n:i.a}}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i})),n.d(e,"b",(function(){return o}));var r=n(12);function i(...t){return o(t)}function o(t){return 0===t.length?r.a:1===t.length?t[0]:function(e){return t.reduce((t,e)=>e(t),e)}}},,,function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t,e){return t=null!=t?t:s,Object(r.b)((n,r)=>{let o,s=!0;n.subscribe(new i.a(r,n=>{(s&&(o=n,1)||!t(o,o=e?e(n):n))&&r.next(n),s=!1}))})}function s(t,e){return t===e}},function(t,e,n){"use strict";function r(t){const e=t(t=>{Error.call(t),t.name=t.constructor.name,t.stack=(new Error).stack});return e.prototype=Object.create(Error.prototype),e.prototype.constructor=e,e}n.d(e,"a",(function(){return r}))},function(t,e,n){"use strict";n.d(e,"a",(function(){return r}));const r={now:()=>(r.delegate||Date).now(),delegate:void 0}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(11);const{isArray:i}=Array;function o(t){return Object(r.a)(e=>function(t,e){return i(e)?t(...e):t(e)}(t,e))}},function(t,e,n){"use strict";n.d(e,"a",(function(){return r}));const r=t=>e=>{for(let n=0,r=t.length;n0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=i()(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=i()(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),document.activeElement.blur(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":o(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}(),u=n(1),a=n.n(u),l=n(2),f=n.n(l),d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},h=function(){function t(t,e){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=f()(t,"click",(function(t){return e.onClick(t)}))}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new c({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return p("action",t)}},{key:"defaultTarget",value:function(t){var e=p("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return p("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach((function(t){n=n&&!!document.queryCommandSupported(t)})),n}}]),e}(a.a);function p(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}e.default=b}]).default},t.exports=r()},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(14),i=n(22),o=n(31);function s(...t){let e=t[t.length-1];return Object(r.a)(e)?(t.pop(),Object(o.a)(t,e)):Object(i.a)(t)}},function(t,e,n){"use strict";n.d(e,"a",(function(){return b}));var r=n(6),i=n(14);const{isArray:o}=Array,{getPrototypeOf:s,prototype:c,keys:u}=Object;function a(t){if(1===t.length){const n=t[0];if(o(n))return{args:n,keys:null};if((e=n)&&"object"==typeof e&&s(e)===c){const t=u(n);return{args:t.map(t=>n[t]),keys:t}}}var e;return{args:t,keys:null}}var l=n(25),f=n(9),d=n(12),h=n(39);function b(...t){let e=void 0,n=void 0;Object(i.a)(t[t.length-1])&&(n=t.pop()),"function"==typeof t[t.length-1]&&(e=t.pop());const{args:o,keys:s}=a(t),c=new r.a(function(t,e,n=d.a){return r=>{v(e,()=>{const{length:i}=t,o=new Array(i);let s=i;const c=t.map(()=>!1);let u=!0;for(let a=0;a{Object(f.a)(t[a],e).subscribe(new p(r,t=>{o[a]=t,u&&(c[a]=!0,u=!c.every(d.a)),u||r.next(n(o.slice()))},()=>0==--s))},r)}},r)}}(o,n,s?t=>{const e={};for(let n=0;n0},t.prototype.connect_=function(){r&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),c?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},t.prototype.disconnect_=function(){r&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},t.prototype.onTransitionEnd_=function(t){var e=t.propertyName,n=void 0===e?"":e;s.some((function(t){return!!~n.indexOf(t)}))&&this.refresh()},t.getInstance=function(){return this.instance_||(this.instance_=new t),this.instance_},t.instance_=null,t}(),a=function(t,e){for(var n=0,r=Object.keys(e);n0},t}(),_="undefined"!=typeof WeakMap?new WeakMap:new n,E=function t(e){if(!(this instanceof t))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var n=u.getInstance(),r=new g(e,n,this);_.set(this,r)};["observe","unobserve","disconnect"].forEach((function(t){E.prototype[t]=function(){var e;return(e=_.get(this))[t].apply(e,arguments)}}));var O=void 0!==i.ResizeObserver?i.ResizeObserver:E;e.a=O}).call(this,n(70))},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(6),i=n(9);function o(t){return new r.a(e=>{let n;try{n=t()}catch(t){return void e.error(t)}return Object(i.a)(n).subscribe(e)})}},function(t,e,n){"use strict"; +/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var r=/["'&<>]/;t.exports=function(t){var e,n=""+t,i=r.exec(n);if(!i)return n;var o="",s=0,c=0;for(s=i.index;s{e.subscribe(n),n.add(t)})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return c}));var r=n(21),i=n(1),o=n(3),s=n(12);function c(t,e,n){const c=Object(r.a)(t)||e||n?{next:t,error:e,complete:n}:t;return c?Object(i.b)((t,e)=>{t.subscribe(new o.a(e,t=>{var n;null===(n=c.next)||void 0===n||n.call(c,t),e.next(t)},t=>{var n;null===(n=c.error)||void 0===n||n.call(c,t),e.error(t)},()=>{var t;null===(t=c.complete)||void 0===t||t.call(c),e.complete()}))}):s.a}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t,e){const n=arguments.length>=2;return Object(r.b)((r,o)=>{let s=n,c=e,u=0;r.subscribe(new i.a(o,e=>{const n=u++;o.next(c=s?t(c,e,n):(s=!0,e))}))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t,e=0){return Object(r.b)((n,r)=>{n.subscribe(new i.a(r,n=>r.add(t.schedule(()=>r.next(n),e)),n=>r.add(t.schedule(()=>r.error(n),e)),()=>r.add(t.schedule(()=>r.complete(),e))))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return a}));var r=n(43),i=n(7);const o={schedule(t){let e=requestAnimationFrame,n=cancelAnimationFrame;const{delegate:r}=o;r&&(e=r.requestAnimationFrame,n=r.cancelAnimationFrame);const s=e(e=>{n=void 0,t(e)});return new i.b(()=>null==n?void 0:n(s))},requestAnimationFrame(...t){const{delegate:e}=o;return((null==e?void 0:e.requestAnimationFrame)||requestAnimationFrame)(...t)},cancelAnimationFrame(...t){const{delegate:e}=o;return((null==e?void 0:e.cancelAnimationFrame)||cancelAnimationFrame)(...t)},delegate:void 0};class s extends r.a{constructor(t,e){super(t,e),this.scheduler=t,this.work=e}requestAsyncId(t,e,n=0){return null!==n&&n>0?super.requestAsyncId(t,e,n):(t.actions.push(this),t.scheduled||(t.scheduled=o.requestAnimationFrame(()=>t.flush(void 0))))}recycleAsyncId(t,e,n=0){if(null!=n&&n>0||null==n&&this.delay>0)return super.recycleAsyncId(t,e,n);0===t.actions.length&&(o.cancelAnimationFrame(e),t.scheduled=void 0)}}var c=n(44);class u extends c.a{flush(t){this.active=!0,this.scheduled=void 0;const{actions:e}=this;let n,r=-1;t=t||e.shift();let i=e.length;do{if(n=t.execute(t.state,t.delay))break}while(++r{let l;c++,o?l=o.subscribe(a):(o=new r.a(t,e,i),l=o.subscribe(a),s=u.subscribe({next(t){o.next(t)},error(t){const e=o;s=void 0,o=void 0,e.error(t)},complete(){s=void 0,o.complete()}}),s.closed&&(s=void 0)),a.add(()=>{c--,l.unsubscribe(),n&&0===c&&s&&(s.unsubscribe(),s=void 0,o=void 0)})}}(o))}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(36);function i(t,e){return Object(r.a)((n,r)=>e?e(n[t],r[t]):n[t]===r[t])}},function(t,e,n){"use strict";n.d(e,"a",(function(){return u}));var r=n(1),i=n(3),o=n(9),s=n(12),c=n(18);function u(...t){let e;return"function"==typeof t[t.length-1]&&(e=t.pop()),Object(r.b)((n,r)=>{const u=t.length,a=new Array(u);let l=t.map(()=>!1),f=!1;n.subscribe(new i.a(r,t=>{if(f){const n=[t,...a];r.next(e?e(...n):n)}}));for(let e=0;e{a[e]=t,f||l[e]||(l[e]=!0,(f=l.every(s.a))&&(l=null))},void 0,c.a))}})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(1),i=n(3),o=n(16);function s(t,e=null){return e=null!=e?e:t,Object(r.b)((n,r)=>{let s=[],c=0;n.subscribe(new i.a(r,n=>{let i=null;c++%e==0&&s.push([]);for(const e of s)e.push(n),t<=e.length&&(i=null!=i?i:[],i.push(e));if(i)for(const t of i)Object(o.a)(s,t),r.next(t)},void 0,()=>{for(const t of s)r.next(t);r.complete()},()=>{s=null}))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return c}));var r=n(41);var i=n(14),o=n(22);function s(...t){let e;return Object(i.a)(t[t.length-1])&&(e=t.pop()),Object(r.a)(1)(Object(o.a)(t,e))}function c(...t){const e=t[t.length-1];return Object(i.a)(e)?(t.pop(),n=>s(t,n,e)):e=>s(t,e)}},function(t,e,n){"use strict";n.d(e,"a",(function(){return a}));var r=n(6),i=n(28),o=n(30),s=n(21),c=n(39),u=n(22);function a(t,e,n,l){return Object(s.a)(n)&&(l=n,n=void 0),l?a(t,e,n).pipe(Object(c.a)(l)):new r.a(r=>{const s=(...t)=>r.next(t.length>1?t:t[0]);return(c=t)&&"function"==typeof c.addEventListener&&"function"==typeof c.removeEventListener?(t.addEventListener(e,s,n),()=>t.removeEventListener(e,s,n)):function(t){return t&&"function"==typeof t.on&&"function"==typeof t.off}(t)?(t.on(e,s),()=>t.off(e,s)):function(t){return t&&"function"==typeof t.addListener&&"function"==typeof t.removeListener}(t)?(t.addListener(e,s),()=>t.removeListener(e,s)):Object(o.a)(t)?Object(i.a)(t=>a(t,e,n))(Object(u.a)(t)).subscribe(r):void r.error(new TypeError("Invalid event target"));var c})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t){return Object(r.b)((e,n)=>{e.subscribe(new i.a(n,()=>n.next(t)))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(6),i=n(18);const o=new r.a(i.a)},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t,e){return Object(r.b)((n,r)=>{let o=0;n.subscribe(new i.a(r,n=>t.call(e,n,o++)&&r.next(n)))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(32);class i extends r.a{constructor(t){super(),this._value=t}get value(){return this.getValue()}_subscribe(t){const e=super._subscribe(t);return!e.closed&&t.next(this._value),e}getValue(){const{hasError:t,thrownError:e,_value:n}=this;if(t)throw e;return this._throwIfClosed(),n}next(t){super.next(this._value=t)}}},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(26),i=n(1),o=n(3);function s(t){return t<=0?()=>r.a:Object(i.b)((e,n)=>{let r=0;e.subscribe(new o.a(n,e=>{++r<=t&&(n.next(e),t<=r&&n.complete())}))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return c}));var r=n(1),i=n(3),o=n(9);const s={leading:!0,trailing:!1};function c(t,{leading:e,trailing:n}=s){return Object(r.b)((r,s)=>{let c=!1,u=null,a=null;const l=()=>{null==a||a.unsubscribe(),a=null,n&&d()},f=e=>a=Object(o.a)(t(e)).subscribe(new i.a(s,l,void 0,l)),d=()=>{c&&(s.next(u),f(u)),c=!1,u=null};r.subscribe(new i.a(s,t=>{c=!0,u=t,!a&&(e?d():f(t))}))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(20);function i(t,e){return e?Object(r.a)(()=>t,e):Object(r.a)(()=>t)}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t){return Object(r.b)((e,n)=>{let r=!1,o=null;e.subscribe(new i.a(n,t=>{r=!0,o=t}));const s=()=>{if(r){r=!1;const t=o;o=null,n.next(t)}};t.subscribe(new i.a(n,s,void 0,s))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(1),i=n(3);function o(t){return Object(r.b)((e,n)=>{let r=0;e.subscribe(new i.a(n,e=>t===r?n.next(e):r++))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(9),i=n(3),o=n(1);function s(t){return Object(o.b)((e,n)=>{let o,c=null,u=!1;c=e.subscribe(new i.a(n,void 0,i=>{o=Object(r.a)(t(i,s(t)(e))),c?(c.unsubscribe(),c=null,o.subscribe(n)):u=!0})),u&&(c.unsubscribe(),c=null,o.subscribe(n))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(42),i=n(1),o=n(3);function s(t,e=r.a){return Object(i.b)((n,r)=>{let i=!1,s=null,c=null;const u=()=>{i=!1;const t=s;s=null,r.next(t)};n.subscribe(new o.a(r,n=>{null==c||c.unsubscribe(),i=!0,s=n,r.add(c=e.schedule(()=>{c=null,u()},t))},void 0,()=>{i&&u(),r.complete()}))})}},function(t,e,n){"use strict";n.d(e,"a",(function(){return i}));var r=n(28);function i(t,e){return"function"==typeof e?Object(r.a)(t,e,1):Object(r.a)(t,1)}},function(t,e,n){"use strict";n.d(e,"a",(function(){return o}));var r=n(62),i=n(26);function o(t,e=i.a,n=i.a){return Object(r.a)(()=>t()?e:n)}},function(t,e,n){"use strict";n.d(e,"a",(function(){return f}));var r=n(6),i=n(7),o=n(1),s=n(3);function c(){return Object(o.b)((t,e)=>{let n=null;t._refCount++;const r=new s.a(e,void 0,void 0,void 0,()=>{if(!t||t._refCount<=0||0<--t._refCount)return void(n=null);const r=t._connection,i=n;n=null,!r||i&&r!==i||r.unsubscribe(),e.unsubscribe()});t.subscribe(r),r.closed||(n=t.connect())})}class u extends r.a{constructor(t,e){super(),this.source=t,this.subjectFactory=e,this._subject=null,this._refCount=0,this._connection=null}_subscribe(t){return this.getSubject().subscribe(t)}getSubject(){const t=this._subject;return t&&!t.isStopped||(this._subject=this.subjectFactory()),this._subject}_teardown(){this._refCount=0;const{_connection:t}=this;this._subject=this._connection=null,null==t||t.unsubscribe()}connect(){let t=this._connection;if(!t){t=this._connection=new i.b;const e=this.getSubject();t.add(this.source.subscribe(new s.a(e,void 0,t=>{this._teardown(),e.error(t)},()=>{this._teardown(),e.complete()},()=>this._teardown()))),t.closed&&(this._connection=null,t=i.b.EMPTY)}return t}refCount(){return c()(this)}}var a=n(32);function l(){return new a.a}function f(){return t=>c()(function(t,e){const n="function"==typeof t?t:()=>t;return"function"==typeof e?Object(o.b)((t,r)=>{const i=n();e(i).subscribe(r).add(t.subscribe(i))}):t=>{const e=new u(t,n);return Object(o.a)(t)&&(e.lift=t.lift),e.source=t,e.subjectFactory=n,e}}(l)(t))}},function(t,e,n){"use strict";n.d(e,"a",(function(){return u}));var r=n(6),i=n(7),o=n(9);var s=n(1);function c(...t){return Object(s.b)((e,n)=>{(function(...t){let e=void 0;return"function"==typeof t[t.length-1]&&(e=t.pop()),new r.a(n=>{const r=t.map(()=>[]),s=t.map(()=>!1),c=new i.b,u=()=>{if(r.every(t=>t.length>0)){let t=r.map(t=>t.shift());if(e)try{t=e(...t)}catch(t){return void n.error(t)}n.next(t),r.some((t,e)=>0===t.length&&s[e])&&n.complete()}};for(let e=0;!n.closed&&e{r[e].push(t),u()},error:t=>n.error(t),complete:()=>{s[e]=!0,0===r[e].length&&n.complete()}}))}return c})})(e,...t).subscribe(n)})}function u(...t){return c(...t)}},function(t,e,n){"use strict";n.d(e,"a",(function(){return a}));var r=n(14),i=n(41),o=n(22);const{isArray:s}=Array;var c=n(9),u=n(26);function a(...t){let e=1/0,n=void 0;return Object(r.a)(t[t.length-1])&&(n=t.pop()),"number"==typeof t[t.length-1]&&(e=t.pop()),(t=function(t){return 1===t.length&&s(t[0])?t[0]:t}(t)).length?1===t.length?Object(c.a)(t[0]):Object(i.a)(e)(Object(o.a)(t,n)):u.a}},function(t,e,n){"use strict";n.d(e,"a",(function(){return s}));var r=n(42);var i=n(1),o=n(3);function s(t,e=r.a){return Object(i.b)((n,r)=>{const i=(s=t)instanceof Date&&!isNaN(s);var s;let c=!1,u=0,a=i?[]:null;const l=()=>c&&!u&&!(null==a?void 0:a.length)&&r.complete();return i&&(u++,r.add(e.schedule(()=>{if(u--,a){const t=a;a=null;for(const e of t)r.next(e)}l()},+t-e.now()))),n.subscribe(new o.a(r,n=>{i?a?a.push(n):r.next(n):(u++,r.add(e.schedule(()=>{u--,r.next(n),l()},t)))},void 0,()=>{c=!0,l()})),()=>{a=null}})}}]]); +//# sourceMappingURL=vendor.77e55a48.min.js.map \ No newline at end of file diff --git a/v2/assets/javascripts/vendor.77e55a48.min.js.map b/v2/assets/javascripts/vendor.77e55a48.min.js.map new file mode 100644 index 000000000..3fb4244ae --- /dev/null +++ b/v2/assets/javascripts/vendor.77e55a48.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///./node_modules/rxjs/dist/esm/internal/util/lift.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/OperatorSubscriber.js","webpack:///./node_modules/rxjs/dist/esm/internal/Observable.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/UnsubscriptionError.js","webpack:///./node_modules/rxjs/dist/esm/internal/Subscription.js","webpack:///./node_modules/rxjs/dist/esm/internal/symbol/iterator.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isPromise.js","webpack:///./node_modules/tslib/tslib.es6.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/subscribeToAsyncIterable.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduled/scheduled.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isInteropObservable.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduled/scheduleObservable.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduled/schedulePromise.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isIterable.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduled/scheduleIterable.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduled/scheduleAsyncIterable.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/from.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/subscribeToObservable.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/subscribeToPromise.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/subscribeToIterable.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isObject.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/map.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/identity.js","webpack:///./node_modules/rxjs/dist/esm/internal/symbol/observable.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isScheduler.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/arrRemove.js","webpack:///./node_modules/rxjs/dist/esm/internal/config.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/noop.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/switchMap.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isFunction.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/fromArray.js","webpack:///./node_modules/rxjs/dist/esm/internal/Subscriber.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/empty.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/mergeMap.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/reportUnhandledError.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isArrayLike.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduled/scheduleArray.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/ObjectUnsubscribedError.js","webpack:///./node_modules/rxjs/dist/esm/internal/Subject.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/pipe.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/distinctUntilChanged.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/createErrorClass.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/dateTimestampProvider.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/mapOneOrManyArgs.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/subscribeToArray.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/mergeAll.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/async.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/Action.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/intervalProvider.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/AsyncAction.js","webpack:///./node_modules/rxjs/dist/esm/internal/Scheduler.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/AsyncScheduler.js","webpack:///./node_modules/clipboard/dist/clipboard.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/of.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/argsArgArrayOrObject.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/combineLatest.js","webpack:///./node_modules/rxjs/dist/esm/internal/ReplaySubject.js","webpack:///./node_modules/resize-observer-polyfill/dist/ResizeObserver.es.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/defer.js","webpack:///./node_modules/escape-html/index.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/finalize.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/tap.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/scan.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/observeOn.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/animationFrameProvider.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/AnimationFrameAction.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/AnimationFrameScheduler.js","webpack:///./node_modules/rxjs/dist/esm/internal/scheduler/animationFrame.js","webpack:///./node_modules/focus-visible/dist/focus-visible.js","webpack:///(webpack)/buildin/global.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/shareReplay.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/distinctUntilKeyChanged.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/withLatestFrom.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/bufferCount.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/concat.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/concatAll.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/startWith.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/fromEvent.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/mapTo.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/never.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/filter.js","webpack:///./node_modules/rxjs/dist/esm/internal/BehaviorSubject.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/take.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/throttle.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/switchMapTo.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/sample.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/skip.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/catchError.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/debounceTime.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/concatMap.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/iif.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/refCount.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/ConnectableObservable.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/share.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/multicast.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/zipWith.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/zip.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/argsOrArgArray.js","webpack:///./node_modules/rxjs/dist/esm/internal/observable/merge.js","webpack:///./node_modules/rxjs/dist/esm/internal/operators/delay.js","webpack:///./node_modules/rxjs/dist/esm/internal/util/isDate.js"],"names":["hasLift","source","lift","operate","init","liftedSource","this","err","error","TypeError","OperatorSubscriber","destination","onNext","onError","onComplete","onUnsubscribe","super","_next","value","_error","unsubscribe","_complete","_a","closed","call","Observable","subscribe","_subscribe","operator","observable","observerOrNext","complete","subscriber","next","isObserver","add","useDeprecatedSynchronousErrorHandling","_trySubscribe","sink","isStopped","canReportError","promiseCtor","getPromiseCtor","resolve","reject","subscription","operations","length","x","Promise","create","UnsubscriptionError","createErrorClass","_super","errors","message","map","i","toString","join","name","empty","initialTeardown","_parentage","_teardowns","Array","isArray","parent","remove","isFunction","e","teardown","execTeardown","push","_hasParent","_addParent","includes","arrRemove","_removeParent","EMPTY","EMPTY_SUBSCRIPTION","isSubscription","Symbol","iterator","isPromise","then","__awaiter","thisArg","_arguments","P","generator","fulfilled","step","rejected","result","done","apply","Object","__values","o","s","m","__asyncValues","asyncIterator","verb","n","v","d","settle","subscribeToAsyncIterable","asyncIterable","asyncIterable_1","asyncIterable_1_1","e_1","e_1_1","return","process","catch","scheduled","input","scheduler","isInteropObservable","sub","Subscription","schedule","scheduleObservable","schedulePromise","isArrayLike","scheduleArray","isIterable","Error","scheduleIterable","scheduleAsyncIterable","from","obj","obs","subscribeToArray","promise","reportUnhandledError","iterable","item","subscribeTo","project","index","identity","isScheduler","arr","indexOf","splice","config","onUnhandledError","undefined","useDeprecatedNextContext","noop","switchMap","resultSelector","innerSubscriber","isComplete","checkComplete","innerIndex","outerIndex","innerValue","fromArray","Subscriber","EMPTY_OBSERVER","SafeSubscriber","context","bind","defaultErrorHandler","mergeMap","concurrent","Infinity","pipe","a","b","ii","active","buffer","doInnerSub","shift","tryInnerSub","outerSubs","setTimeout","ObjectUnsubscribedError","observers","hasError","thrownError","subject","_throwIfClosed","copy","slice","observer","_checkFinalizedStatuses","_innerSubscribe","_b","fns","pipeFromArray","reduce","prev","fn","distinctUntilChanged","compare","keySelector","defaultCompare","first","createImpl","ctorFunc","instance","constructor","stack","prototype","dateTimestampProvider","now","delegate","Date","mapOneOrManyArgs","args","callOrApply","array","len","mergeAll","asyncScheduler","work","state","delay","intervalProvider","setInterval","handle","clearInterval","pending","id","recycleAsyncId","requestAsyncId","_id","flush","_scheduler","_execute","_delay","errored","errorValue","actions","Scheduler","SchedulerAction","action","execute","factory","modules","installedModules","__webpack_require__","moduleId","exports","module","l","c","getter","defineProperty","enumerable","get","r","toStringTag","t","mode","__esModule","ns","key","object","property","hasOwnProperty","p","element","selectedText","nodeName","focus","isReadOnly","hasAttribute","setAttribute","select","setSelectionRange","removeAttribute","selection","window","getSelection","range","document","createRange","selectNodeContents","removeAllRanges","addRange","E","on","callback","ctx","once","self","listener","off","arguments","_","emit","data","evtArr","evts","liveEvents","TinyEmitter","is","target","type","string","node","addEventListener","destroy","removeEventListener","listenNode","nodeList","forEach","listenNodeList","selector","body","listenSelector","HTMLElement","nodeType","String","closest","_delegate","useCapture","listenerFn","delegateTarget","elements","querySelectorAll","Element","matches","proto","matchesSelector","mozMatchesSelector","msMatchesSelector","oMatchesSelector","webkitMatchesSelector","parentNode","__webpack_exports__","src_select","select_default","_typeof","_createClass","defineProperties","props","descriptor","configurable","writable","Constructor","protoProps","staticProps","clipboard_action","ClipboardAction","options","_classCallCheck","resolveOptions","initSelection","container","emitter","text","trigger","selectFake","selectTarget","_this","isRTL","documentElement","getAttribute","removeFake","fakeHandlerCallback","fakeHandler","fakeElem","createElement","style","fontSize","border","padding","margin","position","yPosition","pageYOffset","scrollTop","top","appendChild","copyText","removeChild","succeeded","execCommand","handleResult","clearSelection","activeElement","blur","set","_action","_target","tiny_emitter","tiny_emitter_default","listen","listen_default","clipboard_typeof","clipboard_createClass","clipboard_Clipboard","_Emitter","Clipboard","clipboard_classCallCheck","ReferenceError","_possibleConstructorReturn","__proto__","getPrototypeOf","listenClick","subClass","superClass","setPrototypeOf","_inherits","defaultAction","defaultTarget","defaultText","_this2","onClick","currentTarget","clipboardAction","getAttributeValue","querySelector","support","queryCommandSupported","suffix","attribute","of","pop","objectProto","keys","getKeys","argsArgArrayOrObject","combineLatest","observables","valueTransform","maybeSchedule","values","hasValues","waitingForFirstValues","every","combineLatestInit","shouldComplete","ReplaySubject","bufferSize","windowTime","timestampProvider","infiniteTimeWindow","Math","max","trimBuffer","adjustedBufferSize","last","MapShim","Map","getIndex","some","entry","class_1","__entries__","delete","entries","has","clear","_i","isBrowser","global$1","global","Function","requestAnimationFrame$1","requestAnimationFrame","transitionKeys","mutationObserverSupported","MutationObserver","ResizeObserverController","connected_","mutationEventsAdded_","mutationsObserver_","observers_","onTransitionEnd_","refresh","leadingCall","trailingCall","lastCallTime","resolvePending","proxy","timeoutCallback","timeStamp","throttle","addObserver","connect_","removeObserver","disconnect_","updateObservers_","activeObservers","filter","gatherActive","hasActive","broadcastActive","observe","attributes","childList","characterData","subtree","disconnect","propertyName","getInstance","instance_","defineConfigurable","getWindowOf","ownerDocument","defaultView","emptyRect","createRectInit","toFloat","parseFloat","getBordersSize","styles","positions","size","getHTMLElementContentRect","clientWidth","clientHeight","getComputedStyle","paddings","positions_1","getPaddings","horizPad","left","right","vertPad","bottom","width","height","boxSizing","round","isDocumentElement","vertScrollbar","horizScrollbar","abs","isSVGGraphicsElement","SVGGraphicsElement","SVGElement","getBBox","getContentRect","bbox","getSVGContentRect","y","ResizeObservation","broadcastWidth","broadcastHeight","contentRect_","isActive","rect","broadcastRect","ResizeObserverEntry","rectInit","Constr","contentRect","DOMRectReadOnly","ResizeObserverSPI","controller","callbackCtx","activeObservations_","observations_","callback_","controller_","callbackCtx_","observations","unobserve","clearActive","observation","WeakMap","ResizeObserver","method","defer","observableFactory","matchHtmlRegExp","escape","str","match","exec","html","lastIndex","charCodeAt","substring","finalize","tap","tapObserver","scan","accumulator","seed","hasSeed","hasState","observeOn","animationFrameProvider","request","cancel","cancelAnimationFrame","timestamp","AsyncAction","AsyncScheduler","count","animationFrameScheduler","applyFocusVisiblePolyfill","scope","hadKeyboardEvent","hadFocusVisibleRecently","hadFocusVisibleRecentlyTimeout","inputTypesWhitelist","search","url","tel","email","password","number","date","month","week","time","datetime","isValidFocusTarget","el","classList","addFocusVisibleClass","contains","onPointerDown","addInitialPointerMoveListeners","onInitialPointerMove","toLowerCase","metaKey","altKey","ctrlKey","visibilityState","tagName","readOnly","isContentEditable","clearTimeout","Node","DOCUMENT_FRAGMENT_NODE","host","DOCUMENT_NODE","event","CustomEvent","createEvent","initCustomEvent","dispatchEvent","g","shareReplay","configOrBufferSize","refCount","useRefCount","innerSub","dest","shareReplayOperator","distinctUntilKeyChanged","withLatestFrom","inputs","otherValues","hasValue","ready","otherSource","bufferCount","startBufferEvery","buffers","toEmit","concat","concatAll","startWith","fromEvent","eventName","handler","sourceObj","isJQueryStyleEventEmitter","addListener","removeListener","isNodeStyleEventEmitter","mapTo","NEVER","predicate","BehaviorSubject","_value","getValue","take","seen","defaultThrottleConfig","leading","trailing","durationSelector","sendValue","throttled","throttlingDone","send","switchMapTo","innerObservable","sample","notifier","lastValue","skip","catchError","handledResult","syncUnsub","debounceTime","dueTime","debounceSubscription","emitLastValue","concatMap","iif","condition","trueResult","falseResult","connection","_refCount","refCounter","sharedConnection","_connection","conn","connect","subjectFactory","_subject","getSubject","_teardown","shareSubjectFactory","Subject","share","subjectOrSubjectFactory","connectable","multicast","sources","completed","tryEmit","zip","zipWith","otherInputs","merge","argsOrArgArray","isAbsoluteDelay","isNaN","absoluteTimeValues"],"mappings":"uFAAO,SAASA,EAAQC,GACpB,MAAgF,mBAAjEA,aAAuC,EAASA,EAAOC,MAEnE,SAASC,EAAQC,GACpB,OAAQH,IACJ,GAAID,EAAQC,GACR,OAAOA,EAAOC,MAAK,SAAUG,GACzB,IACI,OAAOD,EAAKC,EAAcC,MAE9B,MAAOC,GACHD,KAAKE,MAAMD,OAIvB,MAAM,IAAIE,UAAU,2CAf5B,qE,8BCAA,8CACO,MAAMC,UAA2B,IACpC,YAAYC,EAAaC,EAAQC,EAASC,EAAYC,GAClDC,MAAML,GACNL,KAAKS,cAAgBA,EACjBH,IACAN,KAAKW,MAAQ,SAAUC,GACnB,IACIN,EAAOM,GAEX,MAAOX,GACHD,KAAKE,MAAMD,MAInBM,IACAP,KAAKa,OAAS,SAAUZ,GACpB,IACIM,EAAQN,GAEZ,MAAOA,GACHD,KAAKK,YAAYH,MAAMD,GAE3BD,KAAKc,gBAGTN,IACAR,KAAKe,UAAY,WACb,IACIP,IAEJ,MAAOP,GACHD,KAAKK,YAAYH,MAAMD,GAE3BD,KAAKc,gBAIjB,cACI,IAAIE,GACHhB,KAAKiB,SAAyC,QAA7BD,EAAKhB,KAAKS,qBAAkC,IAAPO,GAAyBA,EAAGE,KAAKlB,OACxFU,MAAMI,iB,+BCzCd,qFAMO,MAAMK,EACT,YAAYC,GACJA,IACApB,KAAKqB,WAAaD,GAG1B,KAAKE,GACD,MAAMC,EAAa,IAAIJ,EAGvB,OAFAI,EAAW5B,OAASK,KACpBuB,EAAWD,SAAWA,EACfC,EAEX,UAAUC,EAAgBtB,EAAOuB,GAC7B,MAAMC,GA2EQd,EA3EkBY,IA4EnBZ,aAAiB,KAJtC,SAAoBA,GAChB,OAAOA,GAA+B,mBAAfA,EAAMe,MAA8C,mBAAhBf,EAAMV,OAAkD,mBAAnBU,EAAMa,SAGpDG,CAAWhB,IAAU,YAAeA,GA5EhCY,EAAiB,IAAI,IAAeA,EAAgBtB,EAAOuB,GA2ErH,IAAsBb,EA1Ed,MAAM,SAAEU,EAAQ,OAAE3B,GAAWK,KAM7B,OALA0B,EAAWG,IAAIP,EACTA,EAASJ,KAAKQ,EAAY/B,GAC1BA,GAAU,IAAOmC,sCACb9B,KAAKqB,WAAWK,GAChB1B,KAAK+B,cAAcL,IACtBA,EAEX,cAAcM,GACV,IACI,OAAOhC,KAAKqB,WAAWW,GAE3B,MAAO/B,GACH,GAAI,IAAO6B,sCACP,MAAM7B,GA+Cf,SAAwByB,GAC3B,KAAOA,GAAY,CACf,MAAM,OAAET,EAAM,YAAEZ,EAAW,UAAE4B,GAAcP,EAC3C,GAAIT,GAAUgB,EACV,OAAO,EAEXP,EAAarB,GAAeA,aAAuB,IAAaA,EAAc,KAElF,OAAO,EApDK6B,CAAeF,GAA0B,YAAqB/B,GAAvC+B,EAAK9B,MAAMD,IAI9C,QAAQ0B,EAAMQ,GAEV,OAAO,IADPA,EAAcC,EAAeD,IACN,CAACE,EAASC,KAC7B,IAAIC,EACJA,EAAevC,KAAKoB,UAAWR,IAC3B,IACIe,EAAKf,GAET,MAAOX,GACHqC,EAAOrC,GACPsC,SAA4DA,EAAazB,gBAE9EwB,EAAQD,KAGnB,WAAWX,GACP,IAAIV,EACJ,OAA8B,QAAtBA,EAAKhB,KAAKL,cAA2B,IAAPqB,OAAgB,EAASA,EAAGI,UAAUM,GAEhF,CAAC,OACG,OAAO1B,KAEX,QAAQwC,GACJ,OAAOA,EAAWC,OAAS,YAAcD,EAAd,CAA0BxC,MAAQA,KAEjE,UAAUmC,GAEN,OAAO,IADPA,EAAcC,EAAeD,IACN,CAACE,EAASC,KAC7B,IAAI1B,EACJZ,KAAKoB,UAAWsB,GAAO9B,EAAQ8B,EAAKzC,GAAQqC,EAAOrC,GAAM,IAAMoC,EAAQzB,OAOnF,SAASwB,EAAeD,GACpB,IAAInB,EACJ,OAAgG,QAAxFA,EAAKmB,QAAiDA,EAAc,IAAOQ,eAA4B,IAAP3B,EAAgBA,EAAK2B,QALjIxB,EAAWyB,OAAUxB,GACV,IAAID,EAAWC,I,uJC1EnB,MAAMyB,EAAsB,OAAAC,EAAA,GAAkBC,GAAW,SAA6BC,GACzFD,EAAO/C,MACPA,KAAKiD,QAAUD,EACT,GAAGA,EAAOP,kDAClBO,EAAOE,IAAI,CAACjD,EAAKkD,IAAM,GAAGA,EAAI,MAAMlD,EAAImD,cAAcC,KAAK,UACnD,GACNrD,KAAKsD,KAAO,sBACZtD,KAAKgD,OAASA,I,ICyFcO,E,QA9FzB,MAAM,EACT,YAAYC,GACRxD,KAAKwD,gBAAkBA,EACvBxD,KAAKiB,QAAS,EACdjB,KAAKyD,WAAa,KAClBzD,KAAK0D,WAAa,KAEtB,cACI,IAAIV,EACJ,IAAKhD,KAAKiB,OAAQ,CACdjB,KAAKiB,QAAS,EACd,MAAM,WAAEwC,GAAezD,KACvB,GAAI2D,MAAMC,QAAQH,GACd,IAAK,MAAMI,KAAUJ,EACjBI,EAAOC,OAAO9D,WAIlByD,SAAwDA,EAAWK,OAAO9D,MAE9E,MAAM,gBAAEwD,GAAoBxD,KAC5B,GAAI,OAAA+D,EAAA,GAAWP,GACX,IACIA,IAEJ,MAAOQ,GACHhB,EAASgB,aAAanB,EAAsBmB,EAAEhB,OAAS,CAACgB,GAGhE,MAAM,WAAEN,GAAe1D,KACvB,GAAI0D,EAAY,CACZ1D,KAAK0D,WAAa,KAClB,IAAK,MAAMO,KAAYP,EACnB,IACIQ,EAAaD,GAEjB,MAAOhE,GACH+C,EAASA,QAAuCA,EAAS,GACrD/C,aAAe4C,EACfG,EAAS,IAAIA,KAAW/C,EAAI+C,QAG5BA,EAAOmB,KAAKlE,IAK5B,GAAI+C,EACA,MAAM,IAAIH,EAAoBG,IAI1C,IAAIiB,GACA,IAAIjD,EACJ,GAAIiD,GAAYA,IAAajE,KACzB,GAAIA,KAAKiB,OACLiD,EAAaD,OAEZ,CACD,GAAIA,aAAoB,EAAc,CAClC,GAAIA,EAAShD,QAAUgD,EAASG,WAAWpE,MACvC,OAEJiE,EAASI,WAAWrE,OAEvBA,KAAK0D,WAAwC,QAA1B1C,EAAKhB,KAAK0D,kBAA+B,IAAP1C,EAAgBA,EAAK,IAAImD,KAAKF,IAIhG,WAAWJ,GACP,MAAM,WAAEJ,GAAezD,KACvB,OAAOyD,IAAeI,GAAWF,MAAMC,QAAQH,IAAeA,EAAWa,SAAST,GAEtF,WAAWA,GACP,MAAM,WAAEJ,GAAezD,KACvBA,KAAKyD,WAAaE,MAAMC,QAAQH,IAAeA,EAAWU,KAAKN,GAASJ,GAAcA,EAAa,CAACA,EAAYI,GAAUA,EAE9H,cAAcA,GACV,MAAM,WAAEJ,GAAezD,KACnByD,IAAeI,EACf7D,KAAKyD,WAAa,KAEbE,MAAMC,QAAQH,IACnB,OAAAc,EAAA,GAAUd,EAAYI,GAG9B,OAAOI,GACH,MAAM,WAAEP,GAAe1D,KACvB0D,GAAc,OAAAa,EAAA,GAAUb,EAAYO,GAChCA,aAAoB,GACpBA,EAASO,cAAcxE,OAInC,EAAayE,QAAmBlB,EAG7B,IAAI,GAFGtC,QAAS,EACRsC,GAEJ,MAAMmB,EAAqB,EAAaD,MACxC,SAASE,EAAe/D,GAC3B,OAAQA,aAAiB,GACpBA,GACG,WAAYA,GACY,mBAAjBA,EAAMkD,QACQ,mBAAdlD,EAAMiB,KACgB,mBAAtBjB,EAAME,YAEzB,SAASoD,EAAaD,GACM,mBAAbA,EACPA,IAGAA,EAASnD,gB,oFC7GV,MAAM,EALa,mBAAX8D,QAA0BA,OAAOC,SAGrCD,OAAOC,SAFH,a,wBCFR,SAASC,EAAUlE,GACtB,QAASA,GAAoC,mBAApBA,EAAMQ,WAAkD,mBAAfR,EAAMmE,KCkErE,SAASC,EAAUC,EAASC,EAAYC,EAAGC,GAE9C,OAAO,IAAKD,IAAMA,EAAIxC,WAAU,SAAUN,EAASC,GAC/C,SAAS+C,EAAUzE,GAAS,IAAM0E,EAAKF,EAAUzD,KAAKf,IAAW,MAAOoD,GAAK1B,EAAO0B,IACpF,SAASuB,EAAS3E,GAAS,IAAM0E,EAAKF,EAAiB,MAAExE,IAAW,MAAOoD,GAAK1B,EAAO0B,IACvF,SAASsB,EAAKE,GAJlB,IAAe5E,EAIa4E,EAAOC,KAAOpD,EAAQmD,EAAO5E,QAJ1CA,EAIyD4E,EAAO5E,MAJhDA,aAAiBuE,EAAIvE,EAAQ,IAAIuE,GAAE,SAAU9C,GAAWA,EAAQzB,OAITmE,KAAKM,EAAWE,GAClGD,GAAMF,EAAYA,EAAUM,MAAMT,EAASC,GAAc,KAAKvD,WAgCzCgE,OAAO/C,OAY7B,SAASgD,EAASC,GACrB,IAAIC,EAAsB,mBAAXlB,QAAyBA,OAAOC,SAAUkB,EAAID,GAAKD,EAAEC,GAAI3C,EAAI,EAC5E,GAAI4C,EAAG,OAAOA,EAAE7E,KAAK2E,GACrB,GAAIA,GAAyB,iBAAbA,EAAEpD,OAAqB,MAAO,CAC1Cd,KAAM,WAEF,OADIkE,GAAK1C,GAAK0C,EAAEpD,SAAQoD,OAAI,GACrB,CAAEjF,MAAOiF,GAAKA,EAAE1C,KAAMsC,MAAOI,KAG5C,MAAM,IAAI1F,UAAU2F,EAAI,0BAA4B,mCAwDjD,SAASE,EAAcH,GAC1B,IAAKjB,OAAOqB,cAAe,MAAM,IAAI9F,UAAU,wCAC/C,IAAiCgD,EAA7B4C,EAAIF,EAAEjB,OAAOqB,eACjB,OAAOF,EAAIA,EAAE7E,KAAK2E,IAAMA,EAAqCD,EAASC,GAA2B1C,EAAI,GAAI+C,EAAK,QAASA,EAAK,SAAUA,EAAK,UAAW/C,EAAEyB,OAAOqB,eAAiB,WAAc,OAAOjG,MAASmD,GAC9M,SAAS+C,EAAKC,GAAKhD,EAAEgD,GAAKN,EAAEM,IAAM,SAAUC,GAAK,OAAO,IAAIzD,SAAQ,SAAUN,EAASC,IACvF,SAAgBD,EAASC,EAAQ+D,EAAGD,GAAKzD,QAAQN,QAAQ+D,GAAGrB,MAAK,SAASqB,GAAK/D,EAAQ,CAAEzB,MAAOwF,EAAGX,KAAMY,MAAS/D,IADJgE,CAAOjE,EAASC,GAA7B8D,EAAIP,EAAEM,GAAGC,IAA8BX,KAAMW,EAAExF,YAS3H+E,OAAO/C,OClMzB,SAAS2D,EAAyBC,GACrC,OAAQ9E,KAIZ,SAAiB8E,EAAe9E,GAC5B,IAAI+E,EAAiBC,EACjBC,EAAK3F,EACT,OAAOgE,EAAUhF,UAAM,OAAQ,GAAQ,YACnC,IACI,IAAKyG,EAAkBT,EAAcQ,KAAgBE,QAA0BD,EAAgB9E,QAA2B8D,MAAO,CAC7H,MAAM7E,EAAQ8F,EAAkB9F,MAChCc,EAAWC,KAAKf,IAGxB,MAAOgG,GAASD,EAAM,CAAEzG,MAAO0G,GAC/B,QACI,IACQF,IAAsBA,EAAkBjB,OAASzE,EAAKyF,EAAgBI,gBAAe7F,EAAGE,KAAKuF,IAErG,QAAU,GAAIE,EAAK,MAAMA,EAAIzG,OAEjCwB,EAAWD,eApBXqF,CAAQN,EAAe9E,GAAYqF,MAAM9G,GAAOyB,EAAWxB,MAAMD,K,8BCMlE,SAAS+G,EAAUC,EAAOC,GAC7B,GAAa,MAATD,EAAe,CACf,GCVD,SAA6BA,GAChC,OAAOA,GAA6C,mBAA7BA,EAAM,KDSrBE,CAAoBF,GACpB,OETL,SAA4BA,EAAOC,GACtC,OAAO,IAAI/F,EAAA,EAAWO,IAClB,MAAM0F,EAAM,IAAIC,EAAA,EAShB,OARAD,EAAIvF,IAAIqF,EAAUI,SAAS,KACvB,MAAM/F,EAAa0F,EAAM,OACzBG,EAAIvF,IAAIN,EAAWH,UAAU,CACzB,KAAKR,GAASwG,EAAIvF,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWC,KAAKf,MAC/D,MAAMX,GAAOmH,EAAIvF,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWxB,MAAMD,MAC/D,WAAamH,EAAIvF,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWD,oBAG1D2F,IFFIG,CAAmBN,EAAOC,GAEhC,GAAIpC,EAAUmC,GACf,OGbL,SAAyBA,EAAOC,GACnC,OAAO,IAAI/F,EAAA,EAAWO,IAClB,MAAM0F,EAAM,IAAIC,EAAA,EAShB,OARAD,EAAIvF,IAAIqF,EAAUI,SAAS,IAAML,EAAMlC,KAAKnE,IACxCwG,EAAIvF,IAAIqF,EAAUI,SAAS,KACvB5F,EAAWC,KAAKf,GAChBwG,EAAIvF,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWD,iBAEjDxB,IACCmH,EAAIvF,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWxB,MAAMD,SAE/CmH,IHEII,CAAgBP,EAAOC,GAE7B,GAAI,OAAAO,EAAA,GAAYR,GACjB,OAAO,OAAAS,EAAA,GAAcT,EAAOC,GAE3B,GInBN,SAAoBD,GACvB,OAAOA,GAA2C,mBAA3BA,EAAM,GJkBhBU,CAAWV,IAA2B,iBAAVA,EACjC,OKlBL,SAA0BA,EAAOC,GACpC,IAAKD,EACD,MAAM,IAAIW,MAAM,2BAEpB,OAAO,IAAIzG,EAAA,EAAWO,IAClB,MAAM0F,EAAM,IAAIC,EAAA,EAChB,IAAIxC,EAgCJ,OA/BAuC,EAAIvF,IAAI,KACAgD,GAAuC,mBAApBA,EAASgC,QAC5BhC,EAASgC,WAGjBO,EAAIvF,IAAIqF,EAAUI,SAAS,KACvBzC,EAAWoC,EAAM,KACjBG,EAAIvF,IAAIqF,EAAUI,UAAS,WACvB,GAAI5F,EAAWT,OACX,OAEJ,IAAIL,EACA6E,EACJ,IACI,MAAMD,EAASX,EAASlD,OACxBf,EAAQ4E,EAAO5E,MACf6E,EAAOD,EAAOC,KAElB,MAAOxF,GAEH,YADAyB,EAAWxB,MAAMD,GAGjBwF,EACA/D,EAAWD,YAGXC,EAAWC,KAAKf,GAChBZ,KAAKsH,mBAIVF,ILpBIS,CAAiBZ,EAAOC,GAE9B,GAAItC,QAAUA,OAAOqB,eAAwD,mBAAhCgB,EAAMrC,OAAOqB,eAC3D,OMtBL,SAA+BgB,EAAOC,GACzC,IAAKD,EACD,MAAM,IAAIW,MAAM,2BAEpB,OAAO,IAAIzG,EAAA,EAAWO,IAClB,MAAM0F,EAAM,IAAIC,EAAA,EAehB,OAdAD,EAAIvF,IAAIqF,EAAUI,SAAS,KACvB,MAAMzC,EAAWoC,EAAMrC,OAAOqB,iBAC9BmB,EAAIvF,IAAIqF,EAAUI,UAAS,WACvBzC,EAASlD,OAAOoD,KAAKS,IACbA,EAAOC,KACP/D,EAAWD,YAGXC,EAAWC,KAAK6D,EAAO5E,OACvBZ,KAAKsH,qBAKdF,INEIU,CAAsBb,EAAOC,GAG5C,MAAM,IAAI/G,WAAqB,OAAV8G,UAAyBA,GAASA,GAAS,sBOf7D,SAASc,EAAKd,EAAOC,GACxB,OAAKA,EAOMF,EAAUC,EAAOC,GANpBD,aAAiB9F,EAAA,EACV8F,EAEJ,IAAI9F,EAAA,EAMnB,SAAqBqE,GACjB,GAAIA,GAA+C,mBAA9BA,EAAO,KACxB,OCxB8BwC,EDwBDxC,ECxBU9D,IAC3C,MAAMuG,EAAMD,EAAI,OAChB,GAA6B,mBAAlBC,EAAI7G,UACX,MAAM,IAAIjB,UAAU,kEAGpB,OAAO8H,EAAI7G,UAAUM,IDoBpB,GAAI,OAAA+F,EAAA,GAAYjC,GACjB,OAAO,OAAA0C,EAAA,GAAiB1C,GAEvB,GAAIV,EAAUU,GACf,OE9B2B2C,EF8BD3C,EE9Bc9D,IAC5CyG,EAAQpD,KAAMnE,IACLc,EAAWT,SACZS,EAAWC,KAAKf,GAChBc,EAAWD,aAEfxB,GAAQyB,EAAWxB,MAAMD,IACxB8E,KAAK,KAAMqD,EAAA,GACT1G,GFwBF,GAAI8D,GAA6C,mBAA5BA,EAAO,GAC7B,OGjC4B6C,EHiCD7C,EGjCe9D,IAC9C,MAAMmD,EAAWwD,EAAS,KAC1B,OAAG,CACC,IAAIC,EACJ,IACIA,EAAOzD,EAASlD,OAEpB,MAAO1B,GAEH,YADAyB,EAAWxB,MAAMD,GAGrB,GAAIqI,EAAK7C,KAAM,CACX/D,EAAWD,WACX,MAGJ,GADAC,EAAWC,KAAK2G,EAAK1H,OACjBc,EAAWT,OACX,MAUR,MAP+B,mBAApB4D,EAASgC,QAChBnF,EAAWG,IAAI,KACPgD,EAASgC,QACThC,EAASgC,WAIdnF,GHQF,GAAIkD,QAAUA,OAAOqB,eACpBT,GAAkD,mBAAjCA,EAAOZ,OAAOqB,eACjC,OAAOM,EAAyBf,GAE/B,CACD,MAAM5E,EIxCG,QADQ8B,EJyCM8C,IIxCO,iBAAN9C,EJwCS,oBAAsB,IAAI8C,KAG3D,MAAM,IAAIrF,UAFE,gBAAgBS,6GI1C7B,IAAkB8B;;;;;;;;;;;;;;gFDCU,IAAC2F,EDAF,IAACF,EDAE,IAACH,EDgBRO,CAAYtB,M,8BKjB1C,oDAEO,SAAS/D,EAAIsF,EAASvD,GACzB,OAAO,YAAQ,CAACtF,EAAQ+B,KACpB,IAAI+G,EAAQ,EACZ9I,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjDc,EAAWC,KAAK6G,EAAQtH,KAAK+D,EAASrE,EAAO6H,a,6BCNlD,SAASC,EAAShG,GACrB,OAAOA,EADX,mC,6BCAA,kCAAO,MAAMnB,EAAsC,mBAAXqD,QAAyBA,OAAOrD,YAAc,gB,6BCA/E,SAASoH,EAAY/H,GACxB,OAAOA,GAAmC,mBAAnBA,EAAM0G,SADjC,mC,8BCAO,SAAS/C,EAAUqE,EAAKN,GAC3B,GAAIM,EAAK,CACL,MAAMH,EAAQG,EAAIC,QAAQP,GAC1B,GAAKG,GAASG,EAAIE,OAAOL,EAAO,IAHxC,mC,6BCAA,kCAAO,MAAMM,EAAS,CAClBC,iBAAkB,KAClBrG,aAASsG,EACTnH,uCAAuC,EACvCoH,0BAA0B,I,6BCJvB,SAASC,KAAhB,mC,8BCAA,2DAGO,SAASC,EAAUZ,EAASa,GAC/B,OAAO,YAAQ,CAAC1J,EAAQ+B,KACpB,IAAI4H,EAAkB,KAClBb,EAAQ,EACRc,GAAa,EACjB,MAAMC,EAAgB,IAAMD,IAAeD,GAAmB5H,EAAWD,WACzE9B,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjD0I,SAAkEA,EAAgBxI,cAClF,IAAI2I,EAAa,EACbC,EAAajB,IACjB,YAAKD,EAAQ5H,EAAO8I,IAAatI,UAAWkI,EAAkB,IAAI,IAAmB5H,EAAaiI,GAAejI,EAAWC,KAAK0H,EAAiBA,EAAezI,EAAO+I,EAAYD,EAAYD,KAAgBE,QAAaV,EAAW,KACpOK,EAAkB,KAClBE,aAELP,EAAW,KACVM,GAAa,EACbC,W,6BCnBL,SAASzF,EAAWrB,GACvB,MAAoB,mBAANA,EADlB,mC,6BCAA,6DAGO,SAASkH,EAAU3C,EAAOC,GAC7B,OAAKA,EAIM,YAAcD,EAAOC,GAHrB,IAAI,IAAW,YAAiBD,M,+BCL/C,+GAKO,MAAM4C,UAAmB,IAC5B,YAAYxJ,GACRK,QACAV,KAAKiC,WAAY,EACb5B,GACAL,KAAKK,YAAcA,EACf,YAAeA,IACfA,EAAYwB,IAAI7B,OAIpBA,KAAKK,YAAcyJ,EAG3B,cAAcnI,EAAMzB,EAAOuB,GACvB,OAAO,IAAIsI,EAAepI,EAAMzB,EAAOuB,GAE3C,KAAKb,GACIZ,KAAKiC,WACNjC,KAAKW,MAAMC,GAGnB,MAAMX,GACGD,KAAKiC,YACNjC,KAAKiC,WAAY,EACjBjC,KAAKa,OAAOZ,IAGpB,WACSD,KAAKiC,YACNjC,KAAKiC,WAAY,EACjBjC,KAAKe,aAGb,cACSf,KAAKiB,SACNjB,KAAKiC,WAAY,EACjBvB,MAAMI,eAGd,MAAMF,GACFZ,KAAKK,YAAYsB,KAAKf,GAE1B,OAAOX,GACHD,KAAKK,YAAYH,MAAMD,GACvBD,KAAKc,cAET,YACId,KAAKK,YAAYoB,WACjBzB,KAAKc,eAGN,MAAMiJ,UAAuBF,EAChC,YAAYrI,EAAgBtB,EAAOuB,GAG/B,GAFAf,QACAV,KAAKK,YAAcyJ,GACdtI,GAAkBtB,GAASuB,IAAaD,IAAmBsI,EAAgB,CAC5E,IAAInI,EACJ,GAAI,YAAWH,GACXG,EAAOH,OAEN,GAAIA,EAAgB,CAErB,IAAIwI,IADDrI,OAAMzB,QAAOuB,YAAaD,GAEzBxB,MAAQ,IAAOkJ,0BACfc,EAAUrE,OAAO/C,OAAOpB,GACxBwI,EAAQlJ,YAAc,IAAMd,KAAKc,eAGjCkJ,EAAUxI,EAEdG,EAAOA,aAAmC,EAASA,EAAKsI,KAAKD,GAC7D9J,EAAQA,aAAqC,EAASA,EAAM+J,KAAKD,GACjEvI,EAAWA,aAA2C,EAASA,EAASwI,KAAKD,GAEjFhK,KAAKK,YAAc,CACfsB,KAAMA,GAAQ,IACdzB,MAAOA,GAASgK,EAChBzI,SAAUA,GAAY,OAKtC,SAASyI,EAAoBjK,GACzB,GAAI,IAAO6B,sCACP,MAAM7B,EAEV,YAAqBA,GAElB,MAAM6J,EAAiB,CAC1B7I,QAAQ,EACRU,KAAM,IACNzB,MAAOgK,EACPzI,SAAU,M,6BClGd,6CACO,MAAMgD,EAAQ,IAAI,IAAW/C,GAAcA,EAAWD,a,8BCD7D,mEAIO,SAAS0I,EAAS3B,EAASa,EAAgBe,EAAaC,KAC3D,MAA8B,mBAAnBhB,EACC1J,GAAWA,EAAO2K,KAAKH,EAAS,CAACI,EAAGpH,IAAM,YAAKqF,EAAQ+B,EAAGpH,IAAImH,KAAK,YAAI,CAACE,EAAGC,IAAOpB,EAAekB,EAAGC,EAAGrH,EAAGsH,KAAOL,KAE1F,iBAAnBf,IACZe,EAAaf,GAEV,YAAQ,CAAC1J,EAAQ+B,KACpB,IAAI6H,GAAa,EACbmB,EAAS,EACTjC,EAAQ,EACRkC,EAAS,GACb,MAAMnB,EAAgB,IAAMD,IAAemB,GAAUhJ,EAAWD,WAM1DmJ,EAAchK,IAChB8J,IACAhJ,EAAWG,IAAI,YAAK2G,EAAQ5H,EAAO6H,MAAUrH,UAAU,IAAI,IAAmBM,EAAaiI,GAAejI,EAAWC,KAAKgI,QAAaV,EAAW,KAC9IyB,IACAC,EAAOlI,QATK,MAChB,KAAOiI,EAASN,GAAcO,EAAOlI,OAAS,GAC1CmI,EAAWD,EAAOE,UAODC,GACjBtB,SAGR,IAAIuB,EAMJ,OALAA,EAAYpL,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,GAAW8J,EAASN,EAAaQ,EAAWhK,GAAS+J,EAAOxG,KAAKvD,QAASqI,EAAW,KAClJM,GAAa,EACbC,IACAuB,SAAsDA,EAAUjK,iBAE7D,KACH6J,EAAS,W,6BCrCrB,8CACO,SAASvC,EAAqBnI,GACjC+K,WAAW,KACP,MAAM,iBAAEhC,GAAqB,IAC7B,IAAIA,EAIA,MAAM/I,EAHN+I,EAAiB/I,O,6BCL7B,kCAAO,MAAMwH,EAAgB/E,GAAMA,GAAyB,iBAAbA,EAAED,QAAoC,mBAANC,G,6BCA/E,oDAEO,SAASgF,EAAcT,EAAOC,GACjC,OAAO,IAAI,IAAWxF,IAClB,MAAM0F,EAAM,IAAI,IAChB,IAAIjE,EAAI,EAWR,OAVAiE,EAAIvF,IAAIqF,EAAUI,UAAS,WACnBnE,IAAM8D,EAAMxE,QAIhBf,EAAWC,KAAKsF,EAAM9D,MACjBzB,EAAWT,QACZmG,EAAIvF,IAAI7B,KAAKsH,aALb5F,EAAWD,eAQZ2F,M,yFCfR,MAAM6D,EAA0B,OAAAnI,EAAA,GAAkBC,GAAW,WAChEA,EAAO/C,MACPA,KAAKiD,QAAU,wB,YCCZ,MAAM,UAAgB9B,EAAA,EACzB,cACIT,QACAV,KAAKkL,UAAY,GACjBlL,KAAKiB,QAAS,EACdjB,KAAKiC,WAAY,EACjBjC,KAAKmL,UAAW,EAChBnL,KAAKoL,YAAc,KAEvB,KAAK9J,GACD,MAAM+J,EAAU,IAAI,EAAiBrL,KAAMA,MAE3C,OADAqL,EAAQ/J,SAAWA,EACZ+J,EAEX,iBACI,GAAIrL,KAAKiB,OACL,MAAM,IAAIgK,EAGlB,KAAKrK,GAED,GADAZ,KAAKsL,kBACAtL,KAAKiC,UAAW,CACjB,MAAMsJ,EAAOvL,KAAKkL,UAAUM,QAC5B,IAAK,MAAMC,KAAYF,EACnBE,EAAS9J,KAAKf,IAI1B,MAAMX,GAEF,GADAD,KAAKsL,kBACAtL,KAAKiC,UAAW,CACjBjC,KAAKmL,SAAWnL,KAAKiC,WAAY,EACjCjC,KAAKoL,YAAcnL,EACnB,MAAM,UAAEiL,GAAclL,KACtB,KAAOkL,EAAUzI,QACbyI,EAAUL,QAAQ3K,MAAMD,IAIpC,WAEI,GADAD,KAAKsL,kBACAtL,KAAKiC,UAAW,CACjBjC,KAAKiC,WAAY,EACjB,MAAM,UAAEiJ,GAAclL,KACtB,KAAOkL,EAAUzI,QACbyI,EAAUL,QAAQpJ,YAI9B,cACIzB,KAAKiC,UAAYjC,KAAKiB,QAAS,EAC/BjB,KAAKkL,UAAY,KAErB,cAAcxJ,GAEV,OADA1B,KAAKsL,iBACE5K,MAAMqB,cAAcL,GAE/B,WAAWA,GAGP,OAFA1B,KAAKsL,iBACLtL,KAAK0L,wBAAwBhK,GACtB1B,KAAK2L,gBAAgBjK,GAEhC,gBAAgBA,GACZ,MAAM,SAAEyJ,EAAQ,UAAElJ,EAAS,UAAEiJ,GAAclL,KAC3C,OAAOmL,GAAYlJ,EACb,KACCiJ,EAAU/G,KAAKzC,GAAa,IAAI2F,EAAA,EAAa,IAAM,OAAA9C,EAAA,GAAUvE,KAAKkL,UAAWxJ,KAExF,wBAAwBA,GACpB,MAAM,SAAEyJ,EAAQ,YAAEC,EAAW,UAAEnJ,GAAcjC,KACzCmL,EACAzJ,EAAWxB,MAAMkL,GAEZnJ,GACLP,EAAWD,WAGnB,eACI,MAAMF,EAAa,IAAIJ,EAAA,EAEvB,OADAI,EAAW5B,OAASK,KACbuB,GAGf,EAAQqB,OAAS,CAACvC,EAAaV,IACpB,IAAI,EAAiBU,EAAaV,GAEtC,MAAM,UAAyB,EAClC,YAAYU,EAAaV,GACrBe,QACAV,KAAKK,YAAcA,EACnBL,KAAKL,OAASA,EAElB,KAAKiB,GACD,IAAII,EAAI4K,EACwE,QAA/EA,EAAiC,QAA3B5K,EAAKhB,KAAKK,mBAAgC,IAAPW,OAAgB,EAASA,EAAGW,YAAyB,IAAPiK,GAAyBA,EAAG1K,KAAKF,EAAIJ,GAEjI,MAAMX,GACF,IAAIe,EAAI4K,EACyE,QAAhFA,EAAiC,QAA3B5K,EAAKhB,KAAKK,mBAAgC,IAAPW,OAAgB,EAASA,EAAGd,aAA0B,IAAP0L,GAAyBA,EAAG1K,KAAKF,EAAIf,GAElI,WACI,IAAIe,EAAI4K,EAC4E,QAAnFA,EAAiC,QAA3B5K,EAAKhB,KAAKK,mBAAgC,IAAPW,OAAgB,EAASA,EAAGS,gBAA6B,IAAPmK,GAAyBA,EAAG1K,KAAKF,GAEjI,WAAWU,GACP,IAAIV,EAAI4K,EACR,OAAmG,QAA3FA,EAA4B,QAAtB5K,EAAKhB,KAAKL,cAA2B,IAAPqB,OAAgB,EAASA,EAAGI,UAAUM,UAAgC,IAAPkK,EAAgBA,EAAK,O,6BC9GxI,gFACO,SAAStB,KAAQuB,GACpB,OAAOC,EAAcD,GAElB,SAASC,EAAcD,GAC1B,OAAmB,IAAfA,EAAIpJ,OACG,IAEQ,IAAfoJ,EAAIpJ,OACGoJ,EAAI,GAER,SAAe5E,GAClB,OAAO4E,EAAIE,OAAO,CAACC,EAAMC,IAAOA,EAAGD,GAAO/E,M,+BCZlD,oDAEO,SAASiF,EAAqBC,EAASC,GAE1C,OADAD,EAAUA,QAAyCA,EAAUE,EACtD,YAAQ,CAAC1M,EAAQ+B,KACpB,IAAIsK,EACAM,GAAQ,EACZ3M,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,KAC/C0L,IAAWN,EAAOpL,EAAQ,KAAQuL,EAAQH,EAAOA,EAAOI,EAAcA,EAAYxL,GAASA,KACzFc,EAAWC,KAAKf,GACpB0L,GAAQ,OAIpB,SAASD,EAAe9B,EAAGC,GACvB,OAAOD,IAAMC,I,6BCfV,SAAS1H,EAAiByJ,GAC7B,MAKMC,EAAWD,EALDE,IACZ7E,MAAM1G,KAAKuL,GACXA,EAASnJ,KAAOmJ,EAASC,YAAYpJ,KACrCmJ,EAASE,OAAQ,IAAI/E,OAAQ+E,QAKjC,OAFAH,EAASI,UAAYjH,OAAO/C,OAAOgF,MAAMgF,WACzCJ,EAASI,UAAUF,YAAcF,EAC1BA,EATX,mC,6BCAA,kCAAO,MAAMK,EAAwB,CACjCC,IAAG,KACSD,EAAsBE,UAAYC,MAAMF,MAEpDC,cAAU9D,I,6BCJd,8CACA,MAAM,QAAErF,GAAYD,MAIb,SAASsJ,EAAiBhB,GAC7B,OAAO,YAAIiB,GAJf,SAAqBjB,EAAIiB,GACrB,OAAOtJ,EAAQsJ,GAAQjB,KAAMiB,GAAQjB,EAAGiB,GAGrBC,CAAYlB,EAAIiB,M,6BCNvC,kCAAO,MAAMhF,EAAoBkF,GAAW1L,IACxC,IAAK,IAAIyB,EAAI,EAAGkK,EAAMD,EAAM3K,OAAQU,EAAIkK,IAAQ3L,EAAWT,OAAQkC,IAC/DzB,EAAWC,KAAKyL,EAAMjK,IAE1BzB,EAAWD,a,6BCJf,sDAEO,SAAS6L,EAASlD,EAAaC,KAClC,OAAO,YAAS,IAAUD,K,6BCH9B,8CAEO,MAAMmD,EAAiB,IAF9B,MAEkC,GAAe,M,0ECD1C,MAAM,UAAelG,EAAA,EACxB,YAAYH,EAAWsG,GACnB9M,QAEJ,SAAS+M,EAAOC,EAAQ,GACpB,OAAO1N,MCNR,MAAM2N,EAAmB,CAC5B,eAAeT,GACX,MAAM,SAAEH,GAAaY,EACrB,QAASZ,aAA2C,EAASA,EAASa,cAAgBA,gBAAgBV,IAE1G,cAAcW,GACV,MAAM,SAAEd,GAAaY,EACrB,QAASZ,aAA2C,EAASA,EAASe,gBAAkBA,eAAeD,IAE3Gd,cAAU9D,G,YCNP,MAAM,UAAoB,EAC7B,YAAY/B,EAAWsG,GACnB9M,MAAMwG,EAAWsG,GACjBxN,KAAKkH,UAAYA,EACjBlH,KAAKwN,KAAOA,EACZxN,KAAK+N,SAAU,EAEnB,SAASN,EAAOC,EAAQ,GACpB,GAAI1N,KAAKiB,OACL,OAAOjB,KAEXA,KAAKyN,MAAQA,EACb,MAAMO,EAAKhO,KAAKgO,GACV9G,EAAYlH,KAAKkH,UAOvB,OANU,MAAN8G,IACAhO,KAAKgO,GAAKhO,KAAKiO,eAAe/G,EAAW8G,EAAIN,IAEjD1N,KAAK+N,SAAU,EACf/N,KAAK0N,MAAQA,EACb1N,KAAKgO,GAAKhO,KAAKgO,IAAMhO,KAAKkO,eAAehH,EAAWlH,KAAKgO,GAAIN,GACtD1N,KAEX,eAAekH,EAAWiH,EAAKT,EAAQ,GACnC,OAAOC,EAAiBC,YAAY1G,EAAUkH,MAAMnE,KAAK/C,EAAWlH,MAAO0N,GAE/E,eAAeW,EAAYL,EAAIN,EAAQ,GACnC,GAAa,MAATA,GAAiB1N,KAAK0N,QAAUA,IAA0B,IAAjB1N,KAAK+N,QAC9C,OAAOC,EAEXL,EAAiBG,cAAcE,GAGnC,QAAQP,EAAOC,GACX,GAAI1N,KAAKiB,OACL,OAAO,IAAI2G,MAAM,gCAErB5H,KAAK+N,SAAU,EACf,MAAM7N,EAAQF,KAAKsO,SAASb,EAAOC,GACnC,GAAIxN,EACA,OAAOA,GAEe,IAAjBF,KAAK+N,SAAgC,MAAX/N,KAAKgO,KACpChO,KAAKgO,GAAKhO,KAAKiO,eAAejO,KAAKkH,UAAWlH,KAAKgO,GAAI,OAG/D,SAASP,EAAOc,GACZ,IAAIC,GAAU,EACVC,OAAaxF,EACjB,IACIjJ,KAAKwN,KAAKC,GAEd,MAAOzJ,GACHwK,GAAU,EACVC,IAAgBzK,GAAKA,GAAM,IAAI4D,MAAM5D,GAEzC,GAAIwK,EAEA,OADAxO,KAAKc,cACE2N,EAGf,cACI,IAAKzO,KAAKiB,OAAQ,CACd,MAAM,GAAE+M,EAAE,UAAE9G,GAAclH,MACpB,QAAE0O,GAAYxH,EACpBlH,KAAKwN,KAAOxN,KAAKyN,MAAQzN,KAAKkH,UAAY,KAC1ClH,KAAK+N,SAAU,EACf,OAAAxJ,EAAA,GAAUmK,EAAS1O,MACT,MAANgO,IACAhO,KAAKgO,GAAKhO,KAAKiO,eAAe/G,EAAW8G,EAAI,OAEjDhO,KAAK0N,MAAQ,KACbhN,MAAMI,kB,2ECzEX,MAAM6N,EACT,YAAYC,EAAiB9B,EAAM6B,EAAU7B,KACzC9M,KAAK4O,gBAAkBA,EACvB5O,KAAK8M,IAAMA,EAEf,SAASU,EAAME,EAAQ,EAAGD,GACtB,OAAO,IAAIzN,KAAK4O,gBAAgB5O,KAAMwN,GAAMlG,SAASmG,EAAOC,IAGpEiB,EAAU7B,IAAMD,EAAA,EAAsBC,ICT/B,MAAM,UAAuB6B,EAChC,YAAYC,EAAiB9B,EAAM6B,EAAU7B,KACzCpM,MAAMkO,EAAiB9B,GACvB9M,KAAK0O,QAAU,GACf1O,KAAK0K,QAAS,EACd1K,KAAKgH,eAAYiC,EAErB,MAAM4F,GACF,MAAM,QAAEH,GAAY1O,KACpB,GAAIA,KAAK0K,OAEL,YADAgE,EAAQvK,KAAK0K,GAGjB,IAAI3O,EACJF,KAAK0K,QAAS,EACd,GACI,GAAIxK,EAAQ2O,EAAOC,QAAQD,EAAOpB,MAAOoB,EAAOnB,OAC5C,YAECmB,EAASH,EAAQ7D,SAE1B,GADA7K,KAAK0K,QAAS,EACVxK,EAAO,CACP,KAAO2O,EAASH,EAAQ7D,SACpBgE,EAAO/N,cAEX,MAAMZ,M;;;;;;;ACpBlB,IAAiD6O,IASxC,WACT,OAAgB,SAAUC,GAEhB,IAAIC,EAAmB,GAGvB,SAASC,EAAoBC,GAG5B,GAAGF,EAAiBE,GACnB,OAAOF,EAAiBE,GAAUC,QAGnC,IAAIC,EAASJ,EAAiBE,GAAY,CACzChM,EAAGgM,EACHG,GAAG,EACHF,QAAS,IAUV,OANAJ,EAAQG,GAAUjO,KAAKmO,EAAOD,QAASC,EAAQA,EAAOD,QAASF,GAG/DG,EAAOC,GAAI,EAGJD,EAAOD,QA0Df,OArDAF,EAAoBnJ,EAAIiJ,EAGxBE,EAAoBK,EAAIN,EAGxBC,EAAoB7I,EAAI,SAAS+I,EAAS9L,EAAMkM,GAC3CN,EAAoBrJ,EAAEuJ,EAAS9L,IAClCqC,OAAO8J,eAAeL,EAAS9L,EAAM,CAAEoM,YAAY,EAAMC,IAAKH,KAKhEN,EAAoBU,EAAI,SAASR,GACX,oBAAXxK,QAA0BA,OAAOiL,aAC1ClK,OAAO8J,eAAeL,EAASxK,OAAOiL,YAAa,CAAEjP,MAAO,WAE7D+E,OAAO8J,eAAeL,EAAS,aAAc,CAAExO,OAAO,KAQvDsO,EAAoBY,EAAI,SAASlP,EAAOmP,GAEvC,GADU,EAAPA,IAAUnP,EAAQsO,EAAoBtO,IAC/B,EAAPmP,EAAU,OAAOnP,EACpB,GAAW,EAAPmP,GAA8B,iBAAVnP,GAAsBA,GAASA,EAAMoP,WAAY,OAAOpP,EAChF,IAAIqP,EAAKtK,OAAO/C,OAAO,MAGvB,GAFAsM,EAAoBU,EAAEK,GACtBtK,OAAO8J,eAAeQ,EAAI,UAAW,CAAEP,YAAY,EAAM9O,MAAOA,IACtD,EAAPmP,GAA4B,iBAATnP,EAAmB,IAAI,IAAIsP,KAAOtP,EAAOsO,EAAoB7I,EAAE4J,EAAIC,EAAK,SAASA,GAAO,OAAOtP,EAAMsP,IAAQjG,KAAK,KAAMiG,IAC9I,OAAOD,GAIRf,EAAoB/I,EAAI,SAASkJ,GAChC,IAAIG,EAASH,GAAUA,EAAOW,WAC7B,WAAwB,OAAOX,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAH,EAAoB7I,EAAEmJ,EAAQ,IAAKA,GAC5BA,GAIRN,EAAoBrJ,EAAI,SAASsK,EAAQC,GAAY,OAAOzK,OAAOiH,UAAUyD,eAAenP,KAAKiP,EAAQC,IAGzGlB,EAAoBoB,EAAI,GAIjBpB,EAAoBA,EAAoBpJ,EAAI,GAnF7C,CAsFN,CAEJ,SAAUuJ,EAAQD,GA4CxBC,EAAOD,QA1CP,SAAgBmB,GACZ,IAAIC,EAEJ,GAAyB,WAArBD,EAAQE,SACRF,EAAQG,QAERF,EAAeD,EAAQ3P,WAEtB,GAAyB,UAArB2P,EAAQE,UAA6C,aAArBF,EAAQE,SAAyB,CACtE,IAAIE,EAAaJ,EAAQK,aAAa,YAEjCD,GACDJ,EAAQM,aAAa,WAAY,IAGrCN,EAAQO,SACRP,EAAQQ,kBAAkB,EAAGR,EAAQ3P,MAAM6B,QAEtCkO,GACDJ,EAAQS,gBAAgB,YAG5BR,EAAeD,EAAQ3P,UAEtB,CACG2P,EAAQK,aAAa,oBACrBL,EAAQG,QAGZ,IAAIO,EAAYC,OAAOC,eACnBC,EAAQC,SAASC,cAErBF,EAAMG,mBAAmBhB,GACzBU,EAAUO,kBACVP,EAAUQ,SAASL,GAEnBZ,EAAeS,EAAU7N,WAG7B,OAAOoN,IAQL,SAAUnB,EAAQD,GAExB,SAASsC,KAKTA,EAAE9E,UAAY,CACZ+E,GAAI,SAAUrO,EAAMsO,EAAUC,GAC5B,IAAI7N,EAAIhE,KAAKgE,IAAMhE,KAAKgE,EAAI,IAO5B,OALCA,EAAEV,KAAUU,EAAEV,GAAQ,KAAKa,KAAK,CAC/B8H,GAAI2F,EACJC,IAAKA,IAGA7R,MAGT8R,KAAM,SAAUxO,EAAMsO,EAAUC,GAC9B,IAAIE,EAAO/R,KACX,SAASgS,IACPD,EAAKE,IAAI3O,EAAM0O,GACfJ,EAASlM,MAAMmM,EAAKK,WAItB,OADAF,EAASG,EAAIP,EACN5R,KAAK2R,GAAGrO,EAAM0O,EAAUH,IAGjCO,KAAM,SAAU9O,GAMd,IALA,IAAI+O,EAAO,GAAG7G,MAAMtK,KAAKgR,UAAW,GAChCI,IAAWtS,KAAKgE,IAAMhE,KAAKgE,EAAI,KAAKV,IAAS,IAAIkI,QACjDrI,EAAI,EACJkK,EAAMiF,EAAO7P,OAETU,EAAIkK,EAAKlK,IACfmP,EAAOnP,GAAG8I,GAAGvG,MAAM4M,EAAOnP,GAAG0O,IAAKQ,GAGpC,OAAOrS,MAGTiS,IAAK,SAAU3O,EAAMsO,GACnB,IAAI5N,EAAIhE,KAAKgE,IAAMhE,KAAKgE,EAAI,IACxBuO,EAAOvO,EAAEV,GACTkP,EAAa,GAEjB,GAAID,GAAQX,EACV,IAAK,IAAIzO,EAAI,EAAGkK,EAAMkF,EAAK9P,OAAQU,EAAIkK,EAAKlK,IACtCoP,EAAKpP,GAAG8I,KAAO2F,GAAYW,EAAKpP,GAAG8I,GAAGkG,IAAMP,GAC9CY,EAAWrO,KAAKoO,EAAKpP,IAY3B,OAJCqP,EAAiB,OACdxO,EAAEV,GAAQkP,SACHxO,EAAEV,GAENtD,OAIXqP,EAAOD,QAAUsC,EACjBrC,EAAOD,QAAQqD,YAAcf,GAKvB,SAAUrC,EAAQD,EAASF,GAEjC,IAAIwD,EAAKxD,EAAoB,GACzBnC,EAAWmC,EAAoB,GA6FnCG,EAAOD,QAlFP,SAAgBuD,EAAQC,EAAMhB,GAC1B,IAAKe,IAAWC,IAAShB,EACrB,MAAM,IAAIhK,MAAM,8BAGpB,IAAK8K,EAAGG,OAAOD,GACX,MAAM,IAAIzS,UAAU,oCAGxB,IAAKuS,EAAGzG,GAAG2F,GACP,MAAM,IAAIzR,UAAU,qCAGxB,GAAIuS,EAAGI,KAAKH,GACR,OAsBR,SAAoBG,EAAMF,EAAMhB,GAG5B,OAFAkB,EAAKC,iBAAiBH,EAAMhB,GAErB,CACHoB,QAAS,WACLF,EAAKG,oBAAoBL,EAAMhB,KA3B5BsB,CAAWP,EAAQC,EAAMhB,GAE/B,GAAIc,EAAGS,SAASR,GACjB,OAsCR,SAAwBQ,EAAUP,EAAMhB,GAKpC,OAJAjO,MAAMiJ,UAAUwG,QAAQlS,KAAKiS,GAAU,SAASL,GAC5CA,EAAKC,iBAAiBH,EAAMhB,MAGzB,CACHoB,QAAS,WACLrP,MAAMiJ,UAAUwG,QAAQlS,KAAKiS,GAAU,SAASL,GAC5CA,EAAKG,oBAAoBL,EAAMhB,QA9ChCyB,CAAeV,EAAQC,EAAMhB,GAEnC,GAAIc,EAAGG,OAAOF,GACf,OA0DR,SAAwBW,EAAUV,EAAMhB,GACpC,OAAO7E,EAASsE,SAASkC,KAAMD,EAAUV,EAAMhB,GA3DpC4B,CAAeb,EAAQC,EAAMhB,GAGpC,MAAM,IAAIzR,UAAU,+EAgEtB,SAAUkP,EAAQD,GAQxBA,EAAQ0D,KAAO,SAASlS,GACpB,YAAiBqI,IAAVrI,GACAA,aAAiB6S,aACE,IAAnB7S,EAAM8S,UASjBtE,EAAQ+D,SAAW,SAASvS,GACxB,IAAIgS,EAAOjN,OAAOiH,UAAUxJ,SAASlC,KAAKN,GAE1C,YAAiBqI,IAAVrI,IACU,sBAATgS,GAAyC,4BAATA,IAChC,WAAYhS,IACK,IAAjBA,EAAM6B,QAAgB2M,EAAQ0D,KAAKlS,EAAM,MASrDwO,EAAQyD,OAAS,SAASjS,GACtB,MAAwB,iBAAVA,GACPA,aAAiB+S,QAS5BvE,EAAQnD,GAAK,SAASrL,GAGlB,MAAgB,sBAFL+E,OAAOiH,UAAUxJ,SAASlC,KAAKN,KAQxC,SAAUyO,EAAQD,EAASF,GAEjC,IAAI0E,EAAU1E,EAAoB,GAYlC,SAAS2E,EAAUtD,EAAS+C,EAAUV,EAAMhB,EAAUkC,GAClD,IAAIC,EAAa/B,EAAStM,MAAM1F,KAAMkS,WAItC,OAFA3B,EAAQwC,iBAAiBH,EAAMmB,EAAYD,GAEpC,CACHd,QAAS,WACLzC,EAAQ0C,oBAAoBL,EAAMmB,EAAYD,KAgD1D,SAAS9B,EAASzB,EAAS+C,EAAUV,EAAMhB,GACvC,OAAO,SAAS5N,GACZA,EAAEgQ,eAAiBJ,EAAQ5P,EAAE2O,OAAQW,GAEjCtP,EAAEgQ,gBACFpC,EAAS1Q,KAAKqP,EAASvM,IAKnCqL,EAAOD,QA3CP,SAAkB6E,EAAUX,EAAUV,EAAMhB,EAAUkC,GAElD,MAAyC,mBAA9BG,EAASlB,iBACTc,EAAUnO,MAAM,KAAMwM,WAIb,mBAATU,EAGAiB,EAAU5J,KAAK,KAAMoH,UAAU3L,MAAM,KAAMwM,YAI9B,iBAAb+B,IACPA,EAAW5C,SAAS6C,iBAAiBD,IAIlCtQ,MAAMiJ,UAAU1J,IAAIhC,KAAK+S,GAAU,SAAU1D,GAChD,OAAOsD,EAAUtD,EAAS+C,EAAUV,EAAMhB,EAAUkC,SA4BtD,SAAUzE,EAAQD,GAOxB,GAAuB,oBAAZ+E,UAA4BA,QAAQvH,UAAUwH,QAAS,CAC9D,IAAIC,EAAQF,QAAQvH,UAEpByH,EAAMD,QAAUC,EAAMC,iBACND,EAAME,oBACNF,EAAMG,mBACNH,EAAMI,kBACNJ,EAAMK,sBAoB1BrF,EAAOD,QAVP,SAAkBmB,EAAS+C,GACvB,KAAO/C,GAvBc,IAuBHA,EAAQmD,UAAiC,CACvD,GAA+B,mBAApBnD,EAAQ6D,SACf7D,EAAQ6D,QAAQd,GAClB,OAAO/C,EAETA,EAAUA,EAAQoE,cASpB,SAAUtF,EAAQuF,EAAqB1F,GAE7C,aACAA,EAAoBU,EAAEgF,GAGtB,IAAIC,EAAa3F,EAAoB,GACjC4F,EAA8B5F,EAAoB/I,EAAE0O,GAGpDE,EAA4B,mBAAXnQ,QAAoD,iBAApBA,OAAOC,SAAwB,SAAUmD,GAAO,cAAcA,GAAS,SAAUA,GAAO,OAAOA,GAAyB,mBAAXpD,QAAyBoD,EAAI0E,cAAgB9H,QAAUoD,IAAQpD,OAAOgI,UAAY,gBAAkB5E,GAElQgN,EAAe,WAAc,SAASC,EAAiBtC,EAAQuC,GAAS,IAAK,IAAI/R,EAAI,EAAGA,EAAI+R,EAAMzS,OAAQU,IAAK,CAAE,IAAIgS,EAAaD,EAAM/R,GAAIgS,EAAWzF,WAAayF,EAAWzF,aAAc,EAAOyF,EAAWC,cAAe,EAAU,UAAWD,IAAYA,EAAWE,UAAW,GAAM1P,OAAO8J,eAAekD,EAAQwC,EAAWjF,IAAKiF,IAAiB,OAAO,SAAUG,EAAaC,EAAYC,GAAiJ,OAA9HD,GAAYN,EAAiBK,EAAY1I,UAAW2I,GAAiBC,GAAaP,EAAiBK,EAAaE,GAAqBF,GAA7gB,GA8PcG,EAnPM,WAInC,SAASC,EAAgBC,IAb7B,SAAyBlJ,EAAU6I,GAAe,KAAM7I,aAAoB6I,GAAgB,MAAM,IAAInV,UAAU,qCAcxGyV,CAAgB5V,KAAM0V,GAEtB1V,KAAK6V,eAAeF,GACpB3V,KAAK8V,gBAwOT,OA/NAd,EAAaU,EAAiB,CAAC,CAC3BxF,IAAK,iBACLtP,MAAO,WACH,IAAI+U,EAAUzD,UAAUzP,OAAS,QAAsBwG,IAAjBiJ,UAAU,GAAmBA,UAAU,GAAK,GAElFlS,KAAK6O,OAAS8G,EAAQ9G,OACtB7O,KAAK+V,UAAYJ,EAAQI,UACzB/V,KAAKgW,QAAUL,EAAQK,QACvBhW,KAAK2S,OAASgD,EAAQhD,OACtB3S,KAAKiW,KAAON,EAAQM,KACpBjW,KAAKkW,QAAUP,EAAQO,QAEvBlW,KAAKwQ,aAAe,KAQzB,CACCN,IAAK,gBACLtP,MAAO,WACCZ,KAAKiW,KACLjW,KAAKmW,aACEnW,KAAK2S,QACZ3S,KAAKoW,iBASd,CACClG,IAAK,aACLtP,MAAO,WACH,IAAIyV,EAAQrW,KAERsW,EAAwD,OAAhDjF,SAASkF,gBAAgBC,aAAa,OAElDxW,KAAKyW,aAELzW,KAAK0W,oBAAsB,WACvB,OAAOL,EAAMI,cAEjBzW,KAAK2W,YAAc3W,KAAK+V,UAAUhD,iBAAiB,QAAS/S,KAAK0W,uBAAwB,EAEzF1W,KAAK4W,SAAWvF,SAASwF,cAAc,YAEvC7W,KAAK4W,SAASE,MAAMC,SAAW,OAE/B/W,KAAK4W,SAASE,MAAME,OAAS,IAC7BhX,KAAK4W,SAASE,MAAMG,QAAU,IAC9BjX,KAAK4W,SAASE,MAAMI,OAAS,IAE7BlX,KAAK4W,SAASE,MAAMK,SAAW,WAC/BnX,KAAK4W,SAASE,MAAMR,EAAQ,QAAU,QAAU,UAEhD,IAAIc,EAAYlG,OAAOmG,aAAehG,SAASkF,gBAAgBe,UAC/DtX,KAAK4W,SAASE,MAAMS,IAAMH,EAAY,KAEtCpX,KAAK4W,SAAS/F,aAAa,WAAY,IACvC7Q,KAAK4W,SAAShW,MAAQZ,KAAKiW,KAE3BjW,KAAK+V,UAAUyB,YAAYxX,KAAK4W,UAEhC5W,KAAKwQ,aAAesE,IAAiB9U,KAAK4W,UAC1C5W,KAAKyX,aAQV,CACCvH,IAAK,aACLtP,MAAO,WACCZ,KAAK2W,cACL3W,KAAK+V,UAAU9C,oBAAoB,QAASjT,KAAK0W,qBACjD1W,KAAK2W,YAAc,KACnB3W,KAAK0W,oBAAsB,MAG3B1W,KAAK4W,WACL5W,KAAK+V,UAAU2B,YAAY1X,KAAK4W,UAChC5W,KAAK4W,SAAW,QAQzB,CACC1G,IAAK,eACLtP,MAAO,WACHZ,KAAKwQ,aAAesE,IAAiB9U,KAAK2S,QAC1C3S,KAAKyX,aAOV,CACCvH,IAAK,WACLtP,MAAO,WACH,IAAI+W,OAAY,EAEhB,IACIA,EAAYtG,SAASuG,YAAY5X,KAAK6O,QACxC,MAAO5O,GACL0X,GAAY,EAGhB3X,KAAK6X,aAAaF,KAQvB,CACCzH,IAAK,eACLtP,MAAO,SAAsB+W,GACzB3X,KAAKgW,QAAQ5D,KAAKuF,EAAY,UAAY,QAAS,CAC/C9I,OAAQ7O,KAAK6O,OACboH,KAAMjW,KAAKwQ,aACX0F,QAASlW,KAAKkW,QACd4B,eAAgB9X,KAAK8X,eAAe7N,KAAKjK,UAQlD,CACCkQ,IAAK,iBACLtP,MAAO,WACCZ,KAAKkW,SACLlW,KAAKkW,QAAQxF,QAEjBW,SAAS0G,cAAcC,OACvB9G,OAAOC,eAAeK,oBAQ3B,CACCtB,IAAK,UAMLtP,MAAO,WACHZ,KAAKyW,eAEV,CACCvG,IAAK,SACL+H,IAAK,WACD,IAAIpJ,EAASqD,UAAUzP,OAAS,QAAsBwG,IAAjBiJ,UAAU,GAAmBA,UAAU,GAAK,OAIjF,GAFAlS,KAAKkY,QAAUrJ,EAEM,SAAjB7O,KAAKkY,SAAuC,QAAjBlY,KAAKkY,QAChC,MAAM,IAAItQ,MAAM,uDASxB+H,IAAK,WACD,OAAO3P,KAAKkY,UASjB,CACChI,IAAK,SACL+H,IAAK,SAAatF,GACd,QAAe1J,IAAX0J,EAAsB,CACtB,IAAIA,GAA8E,iBAAjD,IAAXA,EAAyB,YAAcoC,EAAQpC,KAA6C,IAApBA,EAAOe,SAWjG,MAAM,IAAI9L,MAAM,+CAVhB,GAAoB,SAAhB5H,KAAK6O,QAAqB8D,EAAO/B,aAAa,YAC9C,MAAM,IAAIhJ,MAAM,qFAGpB,GAAoB,QAAhB5H,KAAK6O,SAAqB8D,EAAO/B,aAAa,aAAe+B,EAAO/B,aAAa,aACjF,MAAM,IAAIhJ,MAAM,0GAGpB5H,KAAKmY,QAAUxF,IAY3BhD,IAAK,WACD,OAAO3P,KAAKmY,YAIbzC,EAhP4B,GAqPnC0C,EAAelJ,EAAoB,GACnCmJ,EAAoCnJ,EAAoB/I,EAAEiS,GAG1DE,EAASpJ,EAAoB,GAC7BqJ,EAA8BrJ,EAAoB/I,EAAEmS,GAGpDE,EAAqC,mBAAX5T,QAAoD,iBAApBA,OAAOC,SAAwB,SAAUmD,GAAO,cAAcA,GAAS,SAAUA,GAAO,OAAOA,GAAyB,mBAAXpD,QAAyBoD,EAAI0E,cAAgB9H,QAAUoD,IAAQpD,OAAOgI,UAAY,gBAAkB5E,GAE3QyQ,EAAwB,WAAc,SAASxD,EAAiBtC,EAAQuC,GAAS,IAAK,IAAI/R,EAAI,EAAGA,EAAI+R,EAAMzS,OAAQU,IAAK,CAAE,IAAIgS,EAAaD,EAAM/R,GAAIgS,EAAWzF,WAAayF,EAAWzF,aAAc,EAAOyF,EAAWC,cAAe,EAAU,UAAWD,IAAYA,EAAWE,UAAW,GAAM1P,OAAO8J,eAAekD,EAAQwC,EAAWjF,IAAKiF,IAAiB,OAAO,SAAUG,EAAaC,EAAYC,GAAiJ,OAA9HD,GAAYN,EAAiBK,EAAY1I,UAAW2I,GAAiBC,GAAaP,EAAiBK,EAAaE,GAAqBF,GAA7gB,GAiBxBoD,EAAsB,SAAUC,GAOhC,SAASC,EAAU1C,EAASP,IAtBhC,SAAkClJ,EAAU6I,GAAe,KAAM7I,aAAoB6I,GAAgB,MAAM,IAAInV,UAAU,qCAuBjH0Y,CAAyB7Y,KAAM4Y,GAE/B,IAAIvC,EAvBZ,SAAoCtE,EAAM7Q,GAAQ,IAAK6Q,EAAQ,MAAM,IAAI+G,eAAe,6DAAgE,OAAO5X,GAAyB,iBAATA,GAAqC,mBAATA,EAA8B6Q,EAAP7Q,EAuB9M6X,CAA2B/Y,MAAO4Y,EAAUI,WAAarT,OAAOsT,eAAeL,IAAY1X,KAAKlB,OAI5G,OAFAqW,EAAMR,eAAeF,GACrBU,EAAM6C,YAAYhD,GACXG,EAsIX,OA/JJ,SAAmB8C,EAAUC,GAAc,GAA0B,mBAAfA,GAA4C,OAAfA,EAAuB,MAAM,IAAIjZ,UAAU,kEAAoEiZ,GAAeD,EAASvM,UAAYjH,OAAO/C,OAAOwW,GAAcA,EAAWxM,UAAW,CAAEF,YAAa,CAAE9L,MAAOuY,EAAUzJ,YAAY,EAAO2F,UAAU,EAAMD,cAAc,KAAegE,IAAYzT,OAAO0T,eAAiB1T,OAAO0T,eAAeF,EAAUC,GAAcD,EAASH,UAAYI,GAY7dE,CAAUV,EAAWD,GAuBrBF,EAAsBG,EAAW,CAAC,CAC9B1I,IAAK,iBACLtP,MAAO,WACH,IAAI+U,EAAUzD,UAAUzP,OAAS,QAAsBwG,IAAjBiJ,UAAU,GAAmBA,UAAU,GAAK,GAElFlS,KAAK6O,OAAmC,mBAAnB8G,EAAQ9G,OAAwB8G,EAAQ9G,OAAS7O,KAAKuZ,cAC3EvZ,KAAK2S,OAAmC,mBAAnBgD,EAAQhD,OAAwBgD,EAAQhD,OAAS3S,KAAKwZ,cAC3ExZ,KAAKiW,KAA+B,mBAAjBN,EAAQM,KAAsBN,EAAQM,KAAOjW,KAAKyZ,YACrEzZ,KAAK+V,UAAoD,WAAxCyC,EAAiB7C,EAAQI,WAA0BJ,EAAQI,UAAY1E,SAASkC,OAQtG,CACCrD,IAAK,cACLtP,MAAO,SAAqBsV,GACxB,IAAIwD,EAAS1Z,KAEbA,KAAKgS,SAAWuG,IAAiBrC,EAAS,SAAS,SAAUlS,GACzD,OAAO0V,EAAOC,QAAQ3V,QAS/B,CACCkM,IAAK,UACLtP,MAAO,SAAiBoD,GACpB,IAAIkS,EAAUlS,EAAEgQ,gBAAkBhQ,EAAE4V,cAEhC5Z,KAAK6Z,kBACL7Z,KAAK6Z,gBAAkB,MAG3B7Z,KAAK6Z,gBAAkB,IAAIpE,EAAiB,CACxC5G,OAAQ7O,KAAK6O,OAAOqH,GACpBvD,OAAQ3S,KAAK2S,OAAOuD,GACpBD,KAAMjW,KAAKiW,KAAKC,GAChBH,UAAW/V,KAAK+V,UAChBG,QAASA,EACTF,QAAShW,SASlB,CACCkQ,IAAK,gBACLtP,MAAO,SAAuBsV,GAC1B,OAAO4D,EAAkB,SAAU5D,KAQxC,CACChG,IAAK,gBACLtP,MAAO,SAAuBsV,GAC1B,IAAI5C,EAAWwG,EAAkB,SAAU5D,GAE3C,GAAI5C,EACA,OAAOjC,SAAS0I,cAAczG,KAUvC,CACCpD,IAAK,cAOLtP,MAAO,SAAqBsV,GACxB,OAAO4D,EAAkB,OAAQ5D,KAOtC,CACChG,IAAK,UACLtP,MAAO,WACHZ,KAAKgS,SAASgB,UAEVhT,KAAK6Z,kBACL7Z,KAAK6Z,gBAAgB7G,UACrBhT,KAAK6Z,gBAAkB,SAG/B,CAAC,CACD3J,IAAK,cACLtP,MAAO,WACH,IAAIiO,EAASqD,UAAUzP,OAAS,QAAsBwG,IAAjBiJ,UAAU,GAAmBA,UAAU,GAAK,CAAC,OAAQ,OAEtFxD,EAA4B,iBAAXG,EAAsB,CAACA,GAAUA,EAClDmL,IAAY3I,SAAS4I,sBAMzB,OAJAvL,EAAQ0E,SAAQ,SAAUvE,GACtBmL,EAAUA,KAAa3I,SAAS4I,sBAAsBpL,MAGnDmL,MAIRpB,EApJe,CAqJxBP,EAAqB9N,GASvB,SAASuP,EAAkBI,EAAQ3J,GAC/B,IAAI4J,EAAY,kBAAoBD,EAEpC,GAAK3J,EAAQK,aAAauJ,GAI1B,OAAO5J,EAAQiG,aAAa2D,GAGavF,EAA6B,QAAI,KAGzD,SAn8BnBvF,EAAOD,QAAUL,K,6BCRnB,8DAGO,SAASqL,KAAMlN,GAClB,IAAIhG,EAAYgG,EAAKA,EAAKzK,OAAS,GACnC,OAAI,YAAYyE,IACZgG,EAAKmN,MACE,YAAcnN,EAAMhG,IAGpB,YAAUgG,K,kFCVzB,MAAM,QAAEtJ,GAAYD,OACd,eAAEsV,EAAgBrM,UAAW0N,EAAaC,KAAMC,GAAY7U,OAC3D,SAAS8U,EAAqBvN,GACjC,GAAoB,IAAhBA,EAAKzK,OAAc,CACnB,MAAM6J,EAAQY,EAAK,GACnB,GAAItJ,EAAQ0I,GACR,MAAO,CAAEY,KAAMZ,EAAOiO,KAAM,MAEhC,IAUQvS,EAVGsE,IAWc,iBAARtE,GAAoBiR,EAAejR,KAASsS,EAX1C,CACf,MAAMC,EAAOC,EAAQlO,GACrB,MAAO,CACHY,KAAMqN,EAAKrX,IAAKgN,GAAQ5D,EAAM4D,IAC9BqK,SAMhB,IAAgBvS,EAFZ,MAAO,CAAEkF,KAAMA,EAAMqN,KAAM,M,mCCTxB,SAASG,KAAiBxN,GAC7B,IAAI7D,OAAiBJ,EACjB/B,OAAY+B,EACZ,OAAAN,EAAA,GAAYuE,EAAKA,EAAKzK,OAAS,MAC/ByE,EAAYgG,EAAKmN,OAEgB,mBAA1BnN,EAAKA,EAAKzK,OAAS,KAC1B4G,EAAiB6D,EAAKmN,OAE1B,MAAQnN,KAAMyN,EAAW,KAAEJ,GAASE,EAAqBvN,GACnD1H,EAAS,IAAIrE,EAAA,EA+BhB,SAA2BwZ,EAAazT,EAAuB0T,EAAiBlS,EAAA,GACnF,OAAQhH,IAyBJmZ,EAAc3T,EAxBW,KACrB,MAAM,OAAEzE,GAAWkY,EACbG,EAAS,IAAInX,MAAMlB,GACzB,IAAIiI,EAASjI,EACb,MAAMsY,EAAYJ,EAAYzX,IAAI,KAAM,GACxC,IAAI8X,GAAwB,EAE5B,IAAK,IAAI7X,EAAI,EAAGA,EAAIV,EAAQU,IAAK,CAc7B0X,EAAc3T,EAbI,KACC,OAAAa,EAAA,GAAK4S,EAAYxX,GAAI+D,GAC7B9F,UAAU,IAAI,EAAwBM,EAAad,IACtDka,EAAO3X,GAAKvC,EACRoa,IACAD,EAAU5X,IAAK,EACf6X,GAAyBD,EAAUE,MAAMvS,EAAA,IAExCsS,GAVEtZ,EAAWC,KAAKiZ,EAAeE,EAAOtP,WAa9C,IAAmB,KAAXd,KAEqBhJ,KAGDA,IAzDjBwZ,CAAkBP,EAAazT,EAAWqT,EAE/DrN,IACG,MAAMtM,EAAQ,GACd,IAAK,IAAIuC,EAAI,EAAGA,EAAI+J,EAAKzK,OAAQU,IAC7BvC,EAAM2Z,EAAKpX,IAAM+J,EAAK/J,GAE1B,OAAOvC,GAGX8H,EAAA,IACR,OAAIW,EACO7D,EAAO8E,KAAK,OAAA2C,EAAA,GAAiB5D,IAEjC7D,EAEX,MAAM,UAAgCqE,EAAA,EAClC,YAAYxJ,EAAaM,EAAOwa,GAC5Bza,MAAML,GACNL,KAAKW,MAAQA,EACbX,KAAKmb,eAAiBA,EAE1B,YACQnb,KAAKmb,iBACLza,MAAMK,YAGNf,KAAKc,eAiCjB,SAAS+Z,EAAc3T,EAAW4H,EAASvM,GACnC2E,EACA3E,EAAaV,IAAIqF,EAAUI,SAASwH,IAGpCA,M,yCClFR,sDAEO,MAAMsM,UAAsB,IAC/B,YAAYC,EAAahR,IAAUiR,EAAajR,IAAUkR,EAAoB,KAC1E7a,QACAV,KAAKqb,WAAaA,EAClBrb,KAAKsb,WAAaA,EAClBtb,KAAKub,kBAAoBA,EACzBvb,KAAK2K,OAAS,GACd3K,KAAKwb,oBAAqB,EAC1Bxb,KAAKwb,mBAAqBF,IAAejR,IACzCrK,KAAKqb,WAAaI,KAAKC,IAAI,EAAGL,GAC9Brb,KAAKsb,WAAaG,KAAKC,IAAI,EAAGJ,GAElC,KAAK1a,GACD,MAAM,UAAEqB,EAAS,OAAE0I,EAAM,mBAAE6Q,EAAkB,kBAAED,EAAiB,WAAED,GAAetb,KAC5EiC,IACD0I,EAAOxG,KAAKvD,IACX4a,GAAsB7Q,EAAOxG,KAAKoX,EAAkBzO,MAAQwO,IAEjEtb,KAAK2b,aACLjb,MAAMiB,KAAKf,GAEf,WAAWc,GACP1B,KAAKsL,iBACLtL,KAAK2b,aACL,MAAMpZ,EAAevC,KAAK2L,gBAAgBjK,IACpC,mBAAE8Z,EAAkB,OAAE7Q,GAAW3K,KACjCuL,EAAOZ,EAAOa,QACpB,IAAK,IAAIrI,EAAI,EAAGA,EAAIoI,EAAK9I,SAAWf,EAAWT,OAAQkC,GAAKqY,EAAqB,EAAI,EACjF9Z,EAAWC,KAAK4J,EAAKpI,IAGzB,OADAnD,KAAK0L,wBAAwBhK,GACtBa,EAEX,aACI,MAAM,WAAE8Y,EAAU,kBAAEE,EAAiB,OAAE5Q,EAAM,mBAAE6Q,GAAuBxb,KAChE4b,GAAsBJ,EAAqB,EAAI,GAAKH,EAE1D,GADAA,EAAahR,KAAYuR,EAAqBjR,EAAOlI,QAAUkI,EAAO7B,OAAO,EAAG6B,EAAOlI,OAASmZ,IAC3FJ,EAAoB,CACrB,MAAM1O,EAAMyO,EAAkBzO,MAC9B,IAAI+O,EAAO,EACX,IAAK,IAAI1Y,EAAI,EAAGA,EAAIwH,EAAOlI,QAAUkI,EAAOxH,IAAM2J,EAAK3J,GAAK,EACxD0Y,EAAO1Y,EAEX0Y,GAAQlR,EAAO7B,OAAO,EAAG+S,EAAO,O,8BC7C5C,YAOA,IAAIC,EAAU,WACV,GAAmB,oBAARC,IACP,OAAOA,IASX,SAASC,EAASpT,EAAKsH,GACnB,IAAI1K,GAAU,EAQd,OAPAoD,EAAIqT,MAAK,SAAUC,EAAOzT,GACtB,OAAIyT,EAAM,KAAOhM,IACb1K,EAASiD,GACF,MAIRjD,EAEX,OAAsB,WAClB,SAAS2W,IACLnc,KAAKoc,YAAc,GAuEvB,OArEAzW,OAAO8J,eAAe0M,EAAQvP,UAAW,OAAQ,CAI7C+C,IAAK,WACD,OAAO3P,KAAKoc,YAAY3Z,QAE5BiN,YAAY,EACZ0F,cAAc,IAMlB+G,EAAQvP,UAAU+C,IAAM,SAAUO,GAC9B,IAAIzH,EAAQuT,EAAShc,KAAKoc,YAAalM,GACnCgM,EAAQlc,KAAKoc,YAAY3T,GAC7B,OAAOyT,GAASA,EAAM,IAO1BC,EAAQvP,UAAUqL,IAAM,SAAU/H,EAAKtP,GACnC,IAAI6H,EAAQuT,EAAShc,KAAKoc,YAAalM,IAClCzH,EACDzI,KAAKoc,YAAY3T,GAAO,GAAK7H,EAG7BZ,KAAKoc,YAAYjY,KAAK,CAAC+L,EAAKtP,KAOpCub,EAAQvP,UAAUyP,OAAS,SAAUnM,GACjC,IAAIoM,EAAUtc,KAAKoc,YACf3T,EAAQuT,EAASM,EAASpM,IACzBzH,GACD6T,EAAQxT,OAAOL,EAAO,IAO9B0T,EAAQvP,UAAU2P,IAAM,SAAUrM,GAC9B,SAAU8L,EAAShc,KAAKoc,YAAalM,IAKzCiM,EAAQvP,UAAU4P,MAAQ,WACtBxc,KAAKoc,YAAYtT,OAAO,IAO5BqT,EAAQvP,UAAUwG,QAAU,SAAUxB,EAAUC,QAChC,IAARA,IAAkBA,EAAM,MAC5B,IAAK,IAAI4K,EAAK,EAAGzb,EAAKhB,KAAKoc,YAAaK,EAAKzb,EAAGyB,OAAQga,IAAM,CAC1D,IAAIP,EAAQlb,EAAGyb,GACf7K,EAAS1Q,KAAK2Q,EAAKqK,EAAM,GAAIA,EAAM,MAGpCC,EAzEU,GAtBX,GAsGVO,EAA8B,oBAAXxL,QAA8C,oBAAbG,UAA4BH,OAAOG,WAAaA,SAGpGsL,OACsB,IAAXC,GAA0BA,EAAOnB,OAASA,KAC1CmB,EAES,oBAAT7K,MAAwBA,KAAK0J,OAASA,KACtC1J,KAEW,oBAAXb,QAA0BA,OAAOuK,OAASA,KAC1CvK,OAGJ2L,SAAS,cAATA,GASPC,EACqC,mBAA1BC,sBAIAA,sBAAsB9S,KAAK0S,GAE/B,SAAU/K,GAAY,OAAO5G,YAAW,WAAc,OAAO4G,EAAS5E,KAAKF,SAAW,IAAO,KAqExG,IAGIkQ,EAAiB,CAAC,MAAO,QAAS,SAAU,OAAQ,QAAS,SAAU,OAAQ,UAE/EC,EAAwD,oBAArBC,iBAInCC,EAA0C,WAM1C,SAASA,IAMLnd,KAAKod,YAAa,EAMlBpd,KAAKqd,sBAAuB,EAM5Brd,KAAKsd,mBAAqB,KAM1Btd,KAAKud,WAAa,GAClBvd,KAAKwd,iBAAmBxd,KAAKwd,iBAAiBvT,KAAKjK,MACnDA,KAAKyd,QAjGb,SAAmB7L,EAAUlE,GACzB,IAAIgQ,GAAc,EAAOC,GAAe,EAAOC,EAAe,EAO9D,SAASC,IACDH,IACAA,GAAc,EACd9L,KAEA+L,GACAG,IAUR,SAASC,IACLjB,EAAwBe,GAO5B,SAASC,IACL,IAAIE,EAAYhR,KAAKF,MACrB,GAAI4Q,EAAa,CAEb,GAAIM,EAAYJ,EA7CN,EA8CN,OAMJD,GAAe,OAGfD,GAAc,EACdC,GAAe,EACf3S,WAAW+S,EAAiBrQ,GAEhCkQ,EAAeI,EAEnB,OAAOF,EA6CYG,CAASje,KAAKyd,QAAQxT,KAAKjK,MAzC9B,IAyMhB,OAxJAmd,EAAyBvQ,UAAUsR,YAAc,SAAUzS,IACjDzL,KAAKud,WAAW1U,QAAQ4C,IAC1BzL,KAAKud,WAAWpZ,KAAKsH,GAGpBzL,KAAKod,YACNpd,KAAKme,YASbhB,EAAyBvQ,UAAUwR,eAAiB,SAAU3S,GAC1D,IAAIP,EAAYlL,KAAKud,WACjB9U,EAAQyC,EAAUrC,QAAQ4C,IAEzBhD,GACDyC,EAAUpC,OAAOL,EAAO,IAGvByC,EAAUzI,QAAUzC,KAAKod,YAC1Bpd,KAAKqe,eASblB,EAAyBvQ,UAAU6Q,QAAU,WACnBzd,KAAKse,oBAIvBte,KAAKyd,WAWbN,EAAyBvQ,UAAU0R,iBAAmB,WAElD,IAAIC,EAAkBve,KAAKud,WAAWiB,QAAO,SAAU/S,GACnD,OAAOA,EAASgT,eAAgBhT,EAASiT,eAQ7C,OADAH,EAAgBnL,SAAQ,SAAU3H,GAAY,OAAOA,EAASkT,qBACvDJ,EAAgB9b,OAAS,GAQpC0a,EAAyBvQ,UAAUuR,SAAW,WAGrCzB,IAAa1c,KAAKod,aAMvB/L,SAAS0B,iBAAiB,gBAAiB/S,KAAKwd,kBAChDtM,OAAO6B,iBAAiB,SAAU/S,KAAKyd,SACnCR,GACAjd,KAAKsd,mBAAqB,IAAIJ,iBAAiBld,KAAKyd,SACpDzd,KAAKsd,mBAAmBsB,QAAQvN,SAAU,CACtCwN,YAAY,EACZC,WAAW,EACXC,eAAe,EACfC,SAAS,MAIb3N,SAAS0B,iBAAiB,qBAAsB/S,KAAKyd,SACrDzd,KAAKqd,sBAAuB,GAEhCrd,KAAKod,YAAa,IAQtBD,EAAyBvQ,UAAUyR,YAAc,WAGxC3B,GAAc1c,KAAKod,aAGxB/L,SAAS4B,oBAAoB,gBAAiBjT,KAAKwd,kBACnDtM,OAAO+B,oBAAoB,SAAUjT,KAAKyd,SACtCzd,KAAKsd,oBACLtd,KAAKsd,mBAAmB2B,aAExBjf,KAAKqd,sBACLhM,SAAS4B,oBAAoB,qBAAsBjT,KAAKyd,SAE5Dzd,KAAKsd,mBAAqB,KAC1Btd,KAAKqd,sBAAuB,EAC5Brd,KAAKod,YAAa,IAStBD,EAAyBvQ,UAAU4Q,iBAAmB,SAAUxc,GAC5D,IAAI4K,EAAK5K,EAAGke,aAAcA,OAAsB,IAAPtT,EAAgB,GAAKA,EAEvCoR,EAAef,MAAK,SAAU/L,GACjD,SAAUgP,EAAarW,QAAQqH,OAG/BlQ,KAAKyd,WAQbN,EAAyBgC,YAAc,WAInC,OAHKnf,KAAKof,YACNpf,KAAKof,UAAY,IAAIjC,GAElBnd,KAAKof,WAOhBjC,EAAyBiC,UAAY,KAC9BjC,EAhMkC,GA0MzCkC,EAAqB,SAAW1M,EAAQuC,GACxC,IAAK,IAAIuH,EAAK,EAAGzb,EAAK2E,OAAO4U,KAAKrF,GAAQuH,EAAKzb,EAAGyB,OAAQga,IAAM,CAC5D,IAAIvM,EAAMlP,EAAGyb,GACb9W,OAAO8J,eAAekD,EAAQzC,EAAK,CAC/BtP,MAAOsU,EAAMhF,GACbR,YAAY,EACZ2F,UAAU,EACVD,cAAc,IAGtB,OAAOzC,GASP2M,EAAc,SAAW3M,GAOzB,OAHkBA,GAAUA,EAAO4M,eAAiB5M,EAAO4M,cAAcC,aAGnD7C,GAItB8C,EAAYC,EAAe,EAAG,EAAG,EAAG,GAOxC,SAASC,EAAQ/e,GACb,OAAOgf,WAAWhf,IAAU,EAShC,SAASif,EAAeC,GAEpB,IADA,IAAIC,EAAY,GACPtD,EAAK,EAAGA,EAAKvK,UAAUzP,OAAQga,IACpCsD,EAAUtD,EAAK,GAAKvK,UAAUuK,GAElC,OAAOsD,EAAUhU,QAAO,SAAUiU,EAAM7I,GAEpC,OAAO6I,EAAOL,EADFG,EAAO,UAAY3I,EAAW,aAE3C,GAmCP,SAAS8I,EAA0BtN,GAG/B,IAAIuN,EAAcvN,EAAOuN,YAAaC,EAAexN,EAAOwN,aAS5D,IAAKD,IAAgBC,EACjB,OAAOV,EAEX,IAAIK,EAASR,EAAY3M,GAAQyN,iBAAiBzN,GAC9C0N,EA3CR,SAAqBP,GAGjB,IAFA,IACIO,EAAW,GACN5D,EAAK,EAAG6D,EAFD,CAAC,MAAO,QAAS,SAAU,QAED7D,EAAK6D,EAAY7d,OAAQga,IAAM,CACrE,IAAItF,EAAWmJ,EAAY7D,GACvB7b,EAAQkf,EAAO,WAAa3I,GAChCkJ,EAASlJ,GAAYwI,EAAQ/e,GAEjC,OAAOyf,EAmCQE,CAAYT,GACvBU,EAAWH,EAASI,KAAOJ,EAASK,MACpCC,EAAUN,EAAS9I,IAAM8I,EAASO,OAKlCC,EAAQlB,EAAQG,EAAOe,OAAQC,EAASnB,EAAQG,EAAOgB,QAqB3D,GAlByB,eAArBhB,EAAOiB,YAOHtF,KAAKuF,MAAMH,EAAQL,KAAcN,IACjCW,GAAShB,EAAeC,EAAQ,OAAQ,SAAWU,GAEnD/E,KAAKuF,MAAMF,EAASH,KAAaR,IACjCW,GAAUjB,EAAeC,EAAQ,MAAO,UAAYa,KAoDhE,SAA2BhO,GACvB,OAAOA,IAAW2M,EAAY3M,GAAQtB,SAASkF,gBA9C1C0K,CAAkBtO,GAAS,CAK5B,IAAIuO,EAAgBzF,KAAKuF,MAAMH,EAAQL,GAAYN,EAC/CiB,EAAiB1F,KAAKuF,MAAMF,EAASH,GAAWR,EAMpB,IAA5B1E,KAAK2F,IAAIF,KACTL,GAASK,GAEoB,IAA7BzF,KAAK2F,IAAID,KACTL,GAAUK,GAGlB,OAAOzB,EAAeW,EAASI,KAAMJ,EAAS9I,IAAKsJ,EAAOC,GAQ9D,IAAIO,EAGkC,oBAAvBC,mBACA,SAAU3O,GAAU,OAAOA,aAAkB2M,EAAY3M,GAAQ2O,oBAKrE,SAAU3O,GAAU,OAAQA,aAAkB2M,EAAY3M,GAAQ4O,YAC3C,mBAAnB5O,EAAO6O,SAiBtB,SAASC,EAAe9O,GACpB,OAAK+J,EAGD2E,EAAqB1O,GAhH7B,SAA2BA,GACvB,IAAI+O,EAAO/O,EAAO6O,UAClB,OAAO9B,EAAe,EAAG,EAAGgC,EAAKb,MAAOa,EAAKZ,QA+GlCa,CAAkBhP,GAEtBsN,EAA0BtN,GALtB8M,EAuCf,SAASC,EAAehd,EAAGkf,EAAGf,EAAOC,GACjC,MAAO,CAAEpe,EAAGA,EAAGkf,EAAGA,EAAGf,MAAOA,EAAOC,OAAQA,GAO/C,IAAIe,EAAmC,WAMnC,SAASA,EAAkBlP,GAMvB3S,KAAK8hB,eAAiB,EAMtB9hB,KAAK+hB,gBAAkB,EAMvB/hB,KAAKgiB,aAAetC,EAAe,EAAG,EAAG,EAAG,GAC5C1f,KAAK2S,OAASA,EA0BlB,OAlBAkP,EAAkBjV,UAAUqV,SAAW,WACnC,IAAIC,EAAOT,EAAezhB,KAAK2S,QAE/B,OADA3S,KAAKgiB,aAAeE,EACZA,EAAKrB,QAAU7gB,KAAK8hB,gBACxBI,EAAKpB,SAAW9gB,KAAK+hB,iBAQ7BF,EAAkBjV,UAAUuV,cAAgB,WACxC,IAAID,EAAOliB,KAAKgiB,aAGhB,OAFAhiB,KAAK8hB,eAAiBI,EAAKrB,MAC3B7gB,KAAK+hB,gBAAkBG,EAAKpB,OACrBoB,GAEJL,EAnD2B,GAsDlCO,EAOA,SAA6BzP,EAAQ0P,GACjC,IA/FoBrhB,EACpB0B,EAAUkf,EAAUf,EAAkBC,EAEtCwB,EACAJ,EA2FIK,GA9FJ7f,GADoB1B,EA+FiBqhB,GA9F9B3f,EAAGkf,EAAI5gB,EAAG4gB,EAAGf,EAAQ7f,EAAG6f,MAAOC,EAAS9f,EAAG8f,OAElDwB,EAAoC,oBAApBE,gBAAkCA,gBAAkB7c,OACpEuc,EAAOvc,OAAO/C,OAAO0f,EAAO1V,WAEhCyS,EAAmB6C,EAAM,CACrBxf,EAAGA,EAAGkf,EAAGA,EAAGf,MAAOA,EAAOC,OAAQA,EAClCvJ,IAAKqK,EACLlB,MAAOhe,EAAIme,EACXD,OAAQE,EAASc,EACjBnB,KAAM/d,IAEHwf,GAyFH7C,EAAmBrf,KAAM,CAAE2S,OAAQA,EAAQ4P,YAAaA,KAK5DE,EAAmC,WAWnC,SAASA,EAAkB7Q,EAAU8Q,EAAYC,GAc7C,GAPA3iB,KAAK4iB,oBAAsB,GAM3B5iB,KAAK6iB,cAAgB,IAAI/G,EACD,mBAAblK,EACP,MAAM,IAAIzR,UAAU,2DAExBH,KAAK8iB,UAAYlR,EACjB5R,KAAK+iB,YAAcL,EACnB1iB,KAAKgjB,aAAeL,EAoHxB,OA5GAF,EAAkB7V,UAAUgS,QAAU,SAAUjM,GAC5C,IAAKT,UAAUzP,OACX,MAAM,IAAItC,UAAU,4CAGxB,GAAuB,oBAAZgU,SAA6BA,mBAAmBxO,OAA3D,CAGA,KAAMgN,aAAkB2M,EAAY3M,GAAQwB,SACxC,MAAM,IAAIhU,UAAU,yCAExB,IAAI8iB,EAAejjB,KAAK6iB,cAEpBI,EAAa1G,IAAI5J,KAGrBsQ,EAAahL,IAAItF,EAAQ,IAAIkP,EAAkBlP,IAC/C3S,KAAK+iB,YAAY7E,YAAYle,MAE7BA,KAAK+iB,YAAYtF,aAQrBgF,EAAkB7V,UAAUsW,UAAY,SAAUvQ,GAC9C,IAAKT,UAAUzP,OACX,MAAM,IAAItC,UAAU,4CAGxB,GAAuB,oBAAZgU,SAA6BA,mBAAmBxO,OAA3D,CAGA,KAAMgN,aAAkB2M,EAAY3M,GAAQwB,SACxC,MAAM,IAAIhU,UAAU,yCAExB,IAAI8iB,EAAejjB,KAAK6iB,cAEnBI,EAAa1G,IAAI5J,KAGtBsQ,EAAa5G,OAAO1J,GACfsQ,EAAajD,MACdhgB,KAAK+iB,YAAY3E,eAAepe,SAQxCyiB,EAAkB7V,UAAUqS,WAAa,WACrCjf,KAAKmjB,cACLnjB,KAAK6iB,cAAcrG,QACnBxc,KAAK+iB,YAAY3E,eAAepe,OAQpCyiB,EAAkB7V,UAAU6R,aAAe,WACvC,IAAIpI,EAAQrW,KACZA,KAAKmjB,cACLnjB,KAAK6iB,cAAczP,SAAQ,SAAUgQ,GAC7BA,EAAYnB,YACZ5L,EAAMuM,oBAAoBze,KAAKif,OAU3CX,EAAkB7V,UAAU+R,gBAAkB,WAE1C,GAAK3e,KAAK0e,YAAV,CAGA,IAAI7M,EAAM7R,KAAKgjB,aAEX1G,EAAUtc,KAAK4iB,oBAAoB1f,KAAI,SAAUkgB,GACjD,OAAO,IAAIhB,EAAoBgB,EAAYzQ,OAAQyQ,EAAYjB,oBAEnEniB,KAAK8iB,UAAU5hB,KAAK2Q,EAAKyK,EAASzK,GAClC7R,KAAKmjB,gBAOTV,EAAkB7V,UAAUuW,YAAc,WACtCnjB,KAAK4iB,oBAAoB9Z,OAAO,IAOpC2Z,EAAkB7V,UAAU8R,UAAY,WACpC,OAAO1e,KAAK4iB,oBAAoBngB,OAAS,GAEtCggB,EAlJ2B,GAwJlCvX,EAA+B,oBAAZmY,QAA0B,IAAIA,QAAY,IAAIvH,EAKjEwH,EAOA,SAASA,EAAe1R,GACpB,KAAM5R,gBAAgBsjB,GAClB,MAAM,IAAInjB,UAAU,sCAExB,IAAK+R,UAAUzP,OACX,MAAM,IAAItC,UAAU,4CAExB,IAAIuiB,EAAavF,EAAyBgC,cACtC1T,EAAW,IAAIgX,EAAkB7Q,EAAU8Q,EAAY1iB,MAC3DkL,EAAU+M,IAAIjY,KAAMyL,IAK5B,CACI,UACA,YACA,cACF2H,SAAQ,SAAUmQ,GAChBD,EAAe1W,UAAU2W,GAAU,WAC/B,IAAIviB,EACJ,OAAQA,EAAKkK,EAAUyE,IAAI3P,OAAOujB,GAAQ7d,MAAM1E,EAAIkR,eAI5D,IAAIzJ,OAEuC,IAA5BkU,EAAS2G,eACT3G,EAAS2G,eAEbA,EAGI,Q,+CC/5Bf,oDAEO,SAASE,EAAMC,GAClB,OAAO,IAAI,IAAW/hB,IAClB,IAAIuF,EACJ,IACIA,EAAQwc,IAEZ,MAAOxjB,GAEH,YADAyB,EAAWxB,MAAMD,GAIrB,OADe,YAAKgH,GACN7F,UAAUM,O;;;;;;;GCEhC,IAAIgiB,EAAkB,UAOtBrU,EAAOD,QAUP,SAAoByD,GAClB,IAOI8Q,EAPAC,EAAM,GAAK/Q,EACXgR,EAAQH,EAAgBI,KAAKF,GAEjC,IAAKC,EACH,OAAOD,EAIT,IAAIG,EAAO,GACPtb,EAAQ,EACRub,EAAY,EAEhB,IAAKvb,EAAQob,EAAMpb,MAAOA,EAAQmb,EAAInhB,OAAQgG,IAAS,CACrD,OAAQmb,EAAIK,WAAWxb,IACrB,KAAK,GACHkb,EAAS,SACT,MACF,KAAK,GACHA,EAAS,QACT,MACF,KAAK,GACHA,EAAS,QACT,MACF,KAAK,GACHA,EAAS,OACT,MACF,KAAK,GACHA,EAAS,OACT,MACF,QACE,SAGAK,IAAcvb,IAChBsb,GAAQH,EAAIM,UAAUF,EAAWvb,IAGnCub,EAAYvb,EAAQ,EACpBsb,GAAQJ,EAGV,OAAOK,IAAcvb,EACjBsb,EAAOH,EAAIM,UAAUF,EAAWvb,GAChCsb,I,6BC5EN,6CACO,SAASI,EAASvS,GACrB,OAAO,YAAQ,CAACjS,EAAQ+B,KACpB/B,EAAOyB,UAAUM,GACjBA,EAAWG,IAAI+P,O,6BCJvB,oEAIO,SAASwS,EAAI5iB,EAAgBtB,EAAOuB,GACvC,MAAM4iB,EAAc,YAAW7iB,IAAmBtB,GAASuB,EAAW,CAAEE,KAAMH,EAAgBtB,QAAOuB,YAAaD,EAClH,OAAO6iB,EACD,YAAQ,CAAC1kB,EAAQ+B,KACf/B,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjD,IAAII,EACwB,QAA3BA,EAAKqjB,EAAY1iB,YAAyB,IAAPX,GAAyBA,EAAGE,KAAKmjB,EAAazjB,GAClFc,EAAWC,KAAKf,IAChBX,IACA,IAAIe,EACyB,QAA5BA,EAAKqjB,EAAYnkB,aAA0B,IAAPc,GAAyBA,EAAGE,KAAKmjB,EAAapkB,GACnFyB,EAAWxB,MAAMD,IAClB,KACC,IAAIe,EAC4B,QAA/BA,EAAKqjB,EAAY5iB,gBAA6B,IAAPT,GAAyBA,EAAGE,KAAKmjB,GACzE3iB,EAAWD,gBAIf,M,6BCvBZ,oDAEO,SAAS6iB,EAAKC,EAAaC,GAC9B,MAAMC,EAAUvS,UAAUzP,QAAU,EACpC,OAAO,YAAQ,CAAC9C,EAAQ+B,KACpB,IAAIgjB,EAAWD,EACXhX,EAAQ+W,EACR/b,EAAQ,EACZ9I,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjD,MAAMuC,EAAIsF,IACV/G,EAAWC,KAAM8L,EAAQiX,EAEjBH,EAAY9W,EAAO7M,EAAOuC,IAExBuhB,GAAW,EAAO9jB,W,6BCdxC,oDAEO,SAAS+jB,EAAUzd,EAAWwG,EAAQ,GACzC,OAAO,YAAQ,CAAC/N,EAAQ+B,KACpB/B,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,GAAUc,EAAWG,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWC,KAAKf,GAAQ8M,IAAUzN,GAAQyB,EAAWG,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWxB,MAAMD,GAAMyN,IAAS,IAAMhM,EAAWG,IAAIqF,EAAUI,SAAS,IAAM5F,EAAWD,WAAYiM,U,kFCH/R,MAAMkX,EAAyB,CAClC,SAAShT,GACL,IAAIiT,EAAU9H,sBACV+H,EAASC,qBACb,MAAM,SAAEhY,GAAa6X,EACjB7X,IACA8X,EAAU9X,EAASgQ,sBACnB+H,EAAS/X,EAASgY,sBAEtB,MAAMlX,EAASgX,EAASG,IACpBF,OAAS7b,EACT2I,EAASoT,KAEb,OAAO,IAAI3d,EAAA,EAAa,IAAMyd,aAAuC,EAASA,EAAOjX,KAEzF,yBAAyBX,GACrB,MAAM,SAAEH,GAAa6X,EACrB,QAAS7X,aAA2C,EAASA,EAASgQ,wBAA0BA,0BAA0B7P,IAE9H,wBAAwBA,GACpB,MAAM,SAAEH,GAAa6X,EACrB,QAAS7X,aAA2C,EAASA,EAASgY,uBAAyBA,yBAAyB7X,IAE5HH,cAAU9D,GCtBP,MAAM,UAA6Bgc,EAAA,EACtC,YAAY/d,EAAWsG,GACnB9M,MAAMwG,EAAWsG,GACjBxN,KAAKkH,UAAYA,EACjBlH,KAAKwN,KAAOA,EAEhB,eAAetG,EAAW8G,EAAIN,EAAQ,GAClC,OAAc,OAAVA,GAAkBA,EAAQ,EACnBhN,MAAMwN,eAAehH,EAAW8G,EAAIN,IAE/CxG,EAAUwH,QAAQvK,KAAKnE,MAChBkH,EAAUF,YAAcE,EAAUF,UAAY4d,EAAuB7H,sBAAsB,IAAM7V,EAAUkH,WAAMnF,MAE5H,eAAe/B,EAAW8G,EAAIN,EAAQ,GAClC,GAAc,MAATA,GAAiBA,EAAQ,GAAgB,MAATA,GAAiB1N,KAAK0N,MAAQ,EAC/D,OAAOhN,MAAMuN,eAAe/G,EAAW8G,EAAIN,GAEd,IAA7BxG,EAAUwH,QAAQjM,SAClBmiB,EAAuBG,qBAAqB/W,GAC5C9G,EAAUF,eAAYiC,I,YCpB3B,MAAM,UAAgCic,EAAA,EACzC,MAAMrW,GACF7O,KAAK0K,QAAS,EACd1K,KAAKgH,eAAYiC,EACjB,MAAM,QAAEyF,GAAY1O,KACpB,IAAIE,EACAuI,GAAS,EACboG,EAASA,GAAUH,EAAQ7D,QAC3B,IAAIsa,EAAQzW,EAAQjM,OACpB,GACI,GAAIvC,EAAQ2O,EAAOC,QAAQD,EAAOpB,MAAOoB,EAAOnB,OAC5C,cAEGjF,EAAQ0c,IAAUtW,EAASH,EAAQ7D,UAE9C,GADA7K,KAAK0K,QAAS,EACVxK,EAAO,CACP,OAASuI,EAAQ0c,IAAUtW,EAASH,EAAQ7D,UACxCgE,EAAO/N,cAEX,MAAMZ,IClBX,MAAMklB,EAA0B,IAAI,EAAwB,I,iBCE3D,WAAe,aASrB,SAASC,EAA0BC,GACjC,IAAIC,GAAmB,EACnBC,GAA0B,EAC1BC,EAAiC,KAEjCC,EAAsB,CACxBzP,MAAM,EACN0P,QAAQ,EACRC,KAAK,EACLC,KAAK,EACLC,OAAO,EACPC,UAAU,EACVC,QAAQ,EACRC,MAAM,EACNC,OAAO,EACPC,MAAM,EACNC,MAAM,EACNC,UAAU,EACV,kBAAkB,GAQpB,SAASC,EAAmBC,GAC1B,SACEA,GACAA,IAAOlV,UACS,SAAhBkV,EAAG9V,UACa,SAAhB8V,EAAG9V,UACH,cAAe8V,GACf,aAAcA,EAAGC,WAsCrB,SAASC,EAAqBF,GACxBA,EAAGC,UAAUE,SAAS,mBAG1BH,EAAGC,UAAU3kB,IAAI,iBACjB0kB,EAAG1V,aAAa,2BAA4B,KA4C9C,SAAS8V,EAAc3iB,GACrBuhB,GAAmB,EAuErB,SAASqB,IACPvV,SAAS0B,iBAAiB,YAAa8T,GACvCxV,SAAS0B,iBAAiB,YAAa8T,GACvCxV,SAAS0B,iBAAiB,UAAW8T,GACrCxV,SAAS0B,iBAAiB,cAAe8T,GACzCxV,SAAS0B,iBAAiB,cAAe8T,GACzCxV,SAAS0B,iBAAiB,YAAa8T,GACvCxV,SAAS0B,iBAAiB,YAAa8T,GACvCxV,SAAS0B,iBAAiB,aAAc8T,GACxCxV,SAAS0B,iBAAiB,WAAY8T,GAsBxC,SAASA,EAAqB7iB,GAGxBA,EAAE2O,OAAOlC,UAAgD,SAApCzM,EAAE2O,OAAOlC,SAASqW,gBAI3CvB,GAAmB,EAzBnBlU,SAAS4B,oBAAoB,YAAa4T,GAC1CxV,SAAS4B,oBAAoB,YAAa4T,GAC1CxV,SAAS4B,oBAAoB,UAAW4T,GACxCxV,SAAS4B,oBAAoB,cAAe4T,GAC5CxV,SAAS4B,oBAAoB,cAAe4T,GAC5CxV,SAAS4B,oBAAoB,YAAa4T,GAC1CxV,SAAS4B,oBAAoB,YAAa4T,GAC1CxV,SAAS4B,oBAAoB,aAAc4T,GAC3CxV,SAAS4B,oBAAoB,WAAY4T,IAwB3CxV,SAAS0B,iBAAiB,WAzI1B,SAAmB/O,GACbA,EAAE+iB,SAAW/iB,EAAEgjB,QAAUhjB,EAAEijB,UAI3BX,EAAmBhB,EAAMvN,gBAC3B0O,EAAqBnB,EAAMvN,eAG7BwN,GAAmB,MAgI2B,GAChDlU,SAAS0B,iBAAiB,YAAa4T,GAAe,GACtDtV,SAAS0B,iBAAiB,cAAe4T,GAAe,GACxDtV,SAAS0B,iBAAiB,aAAc4T,GAAe,GACvDtV,SAAS0B,iBAAiB,oBApE1B,SAA4B/O,GACO,WAA7BqN,SAAS6V,kBAKP1B,IACFD,GAAmB,GAErBqB,QA2D8D,GAElEA,IAMAtB,EAAMvS,iBAAiB,SAtHvB,SAAiB/O,GApFjB,IAAuCuiB,EACjC3T,EACAuU,EAoFCb,EAAmBtiB,EAAE2O,UAItB4S,IA1FiCgB,EA0FiBviB,EAAE2O,OAzFpDC,EAAO2T,EAAG3T,KAGE,WAFZuU,EAAUZ,EAAGY,UAEUzB,EAAoB9S,KAAU2T,EAAGa,UAI5C,aAAZD,IAA2BZ,EAAGa,UAI9Bb,EAAGc,qBA+ELZ,EAAqBziB,EAAE2O,WA+Gc,GACzC2S,EAAMvS,iBAAiB,QAxGvB,SAAgB/O,GA9DhB,IAAiCuiB,EA+D1BD,EAAmBtiB,EAAE2O,UAKxB3O,EAAE2O,OAAO6T,UAAUE,SAAS,kBAC5B1iB,EAAE2O,OAAO/B,aAAa,+BAMtB4U,GAA0B,EAC1BtU,OAAOoW,aAAa7B,GACpBA,EAAiCvU,OAAOlG,YAAW,WACjDwa,GAA0B,IACzB,MA/E0Be,EAgFLviB,EAAE2O,QA/EpB/B,aAAa,8BAGrB2V,EAAGC,UAAU1iB,OAAO,iBACpByiB,EAAGvV,gBAAgB,iCAiKkB,GAOnCsU,EAAM5R,WAAa6T,KAAKC,wBAA0BlC,EAAMmC,KAI1DnC,EAAMmC,KAAK5W,aAAa,wBAAyB,IACxCyU,EAAM5R,WAAa6T,KAAKG,gBACjCrW,SAASkF,gBAAgBiQ,UAAU3kB,IAAI,oBACvCwP,SAASkF,gBAAgB1F,aAAa,wBAAyB,KAOnE,GAAsB,oBAAXK,QAA8C,oBAAbG,SAA0B,CAQpE,IAAIsW,EAJJzW,OAAOmU,0BAA4BA,EAMnC,IACEsC,EAAQ,IAAIC,YAAY,gCACxB,MAAO1nB,IAEPynB,EAAQtW,SAASwW,YAAY,gBACvBC,gBAAgB,gCAAgC,GAAO,EAAO,IAGtE5W,OAAO6W,cAAcJ,GAGC,oBAAbtW,UAGTgU,EAA0BhU,UAnTmCtC,I,cCDjE,IAAIiZ,EAGJA,EAAI,WACH,OAAOhoB,KADJ,GAIJ,IAECgoB,EAAIA,GAAK,IAAInL,SAAS,cAAb,GACR,MAAO7Y,GAEc,iBAAXkN,SAAqB8W,EAAI9W,QAOrC7B,EAAOD,QAAU4Y,G,iCCnBjB,qDAEO,SAASC,EAAYC,EAAoB5M,EAAYpU,GACxD,IAAI6B,EAYJ,OAVIA,EADAmf,GAAoD,iBAAvBA,EACpBA,EAGA,CACL7M,WAAY6M,EACZ5M,aACA6M,UAAU,EACVjhB,aAGD,YAEX,UAA6B,WAAEmU,EAAahR,IAAQ,WAAEiR,EAAajR,IAAU8d,SAAUC,EAAW,UAAElhB,IAChG,IAAImE,EAEA9I,EADA4lB,EAAW,EAEf,MAAO,CAACxoB,EAAQ+B,KAEZ,IAAI2mB,EADJF,IAEK9c,EAqBDgd,EAAWhd,EAAQjK,UAAUM,IApB7B2J,EAAU,IAAI,IAAcgQ,EAAYC,EAAYpU,GACpDmhB,EAAWhd,EAAQjK,UAAUM,GAC7Ba,EAAe5C,EAAOyB,UAAU,CAC5B,KAAKR,GAASyK,EAAQ1J,KAAKf,IAC3B,MAAMX,GACF,MAAMqoB,EAAOjd,EACb9I,OAAe0G,EACfoC,OAAUpC,EACVqf,EAAKpoB,MAAMD,IAEf,WACIsC,OAAe0G,EACfoC,EAAQ5J,cAGZc,EAAatB,SACbsB,OAAe0G,IAMvBvH,EAAWG,IAAI,KACXsmB,IACAE,EAASvnB,cACLsnB,GAA4B,IAAbD,GAAkB5lB,IACjCA,EAAazB,cACbyB,OAAe0G,EACfoC,OAAUpC,MAtCPsf,CAAoBxf,M,6BCfvC,8CACO,SAASyf,EAAwBtY,EAAK/D,GACzC,OAAO,YAAqB,CAACzJ,EAAGkf,IAAMzV,EAAUA,EAAQzJ,EAAEwN,GAAM0R,EAAE1R,IAAQxN,EAAEwN,KAAS0R,EAAE1R,M,6BCF3F,2EAKO,SAASuY,KAAkBC,GAC9B,IAAIlgB,EAIJ,MAHyC,mBAA9BkgB,EAAOA,EAAOjmB,OAAS,KAC9B+F,EAAUkgB,EAAOrO,OAEd,YAAQ,CAAC1a,EAAQ+B,KACpB,MAAM2L,EAAMqb,EAAOjmB,OACbkmB,EAAc,IAAIhlB,MAAM0J,GAC9B,IAAIub,EAAWF,EAAOxlB,IAAI,KAAM,GAC5B2lB,GAAQ,EACZlpB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjD,GAAIioB,EAAO,CACP,MAAM/N,EAAS,CAACla,KAAU+nB,GAC1BjnB,EAAWC,KAAK6G,EAAUA,KAAWsS,GAAUA,OAGvD,IAAK,IAAI3X,EAAI,EAAGA,EAAIkK,EAAKlK,IAAK,CAC1B,MAAM8D,EAAQyhB,EAAOvlB,GACrB,IAAI2lB,EACJ,IACIA,EAAc,YAAK7hB,GAEvB,MAAOhH,GAEH,YADAyB,EAAWxB,MAAMD,GAGrB6oB,EAAY1nB,UAAU,IAAI,IAAmBM,EAAad,IACtD+nB,EAAYxlB,GAAKvC,EACZioB,GAAUD,EAASzlB,KACpBylB,EAASzlB,IAAK,GACb0lB,EAAQD,EAAS3N,MAAM,QAAe2N,EAAW,aAEvD3f,EAAW,W,6BCrC1B,4DAGO,SAAS8f,EAAY1N,EAAY2N,EAAmB,MAEvD,OADAA,EAAmBA,QAA2DA,EAAmB3N,EAC1F,YAAQ,CAAC1b,EAAQ+B,KACpB,IAAIunB,EAAU,GACV9D,EAAQ,EACZxlB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjD,IAAIsoB,EAAS,KACT/D,IAAU6D,GAAqB,GAC/BC,EAAQ9kB,KAAK,IAEjB,IAAK,MAAMwG,KAAUse,EACjBte,EAAOxG,KAAKvD,GACRya,GAAc1Q,EAAOlI,SACrBymB,EAASA,QAAuCA,EAAS,GACzDA,EAAO/kB,KAAKwG,IAGpB,GAAIue,EACA,IAAK,MAAMve,KAAUue,EACjB,YAAUD,EAASte,GACnBjJ,EAAWC,KAAKgJ,SAGzB1B,EAAW,KACV,IAAK,MAAM0B,KAAUse,EACjBvnB,EAAWC,KAAKgJ,GAEpBjJ,EAAWD,YACZ,KACCwnB,EAAU,Y,+FC7Bf,SAASE,KAAUjc,GACtB,IAAIhG,EAIJ,OAHI,OAAAyB,EAAA,GAAYuE,EAAKA,EAAKzK,OAAS,MAC/ByE,EAAYgG,EAAKmN,OCJd,OAAA/M,EAAA,GAAS,EDMT8b,CAAY,OAAAxf,EAAA,GAAUsD,EAAMhG,IENhC,SAASmiB,KAAavO,GACzB,MAAM5T,EAAY4T,EAAOA,EAAOrY,OAAS,GACzC,OAAI,OAAAkG,EAAA,GAAYzB,IACZ4T,EAAOT,MACC1a,GAAWwpB,EAAOrO,EAAQnb,EAAQuH,IAGlCvH,GAAWwpB,EAAOrO,EAAQnb,K,6BCT1C,qFAMO,SAAS2pB,EAAU3W,EAAQ4W,EAAW5T,EAAStM,GAKlD,OAJI,YAAWsM,KACXtM,EAAiBsM,EACjBA,OAAU1M,GAEVI,EACOigB,EAAU3W,EAAQ4W,EAAW5T,GAASrL,KAAK,YAAiBjB,IAEhE,IAAI,IAAY3H,IACnB,MAAM8nB,EAAU,IAAItc,IAASxL,EAAWC,KAAKuL,EAAKzK,OAAS,EAAIyK,EAAOA,EAAK,IAC3E,OAyBeuc,EAzBG9W,IA0BoC,mBAA/B8W,EAAU1W,kBAA4E,mBAAlC0W,EAAUxW,qBAzBjFN,EAAOI,iBAAiBwW,EAAWC,EAAS7T,GACrC,IAAMhD,EAAOM,oBAAoBsW,EAAWC,EAAS7T,IAoBxE,SAAmC8T,GAC/B,OAAOA,GAAqC,mBAAjBA,EAAU9X,IAA8C,mBAAlB8X,EAAUxX,IAnBnEyX,CAA0B/W,IAC1BA,EAAOhB,GAAG4X,EAAWC,GACd,IAAM7W,EAAOV,IAAIsX,EAAWC,IAa/C,SAAiCC,GAC7B,OAAOA,GAA8C,mBAA1BA,EAAUE,aAAkE,mBAA7BF,EAAUG,eAZ5EC,CAAwBlX,IACxBA,EAAOgX,YAAYJ,EAAWC,GACvB,IAAM7W,EAAOiX,eAAeL,EAAWC,IAE9C,YAAY7W,GACL,YAAUA,GAAW2W,EAAU3W,EAAQ4W,EAAW5T,GAAlD,CAA4D,YAAUhD,IAASvR,UAAUM,QAEpGA,EAAWxB,MAAM,IAAIC,UAAU,yBAUvC,IAAuBspB,M,6BCzCvB,oDAEO,SAASK,EAAMlpB,GAClB,OAAO,YAAQ,CAACjB,EAAQ+B,KACpB/B,EAAOyB,UAAU,IAAI,IAAmBM,EAAY,IAAMA,EAAWC,KAAKf,S,6BCJlF,qDAEO,MAAMmpB,EAAQ,IAAI,IAAW,M,6BCFpC,oDAEO,SAASvL,EAAOwL,EAAW/kB,GAC9B,OAAO,YAAQ,CAACtF,EAAQ+B,KACpB,IAAI+G,EAAQ,EACZ9I,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,GAAUopB,EAAU9oB,KAAK+D,EAASrE,EAAO6H,MAAY/G,EAAWC,KAAKf,S,6BCLlI,8CACO,MAAMqpB,UAAwB,IACjC,YAAYC,GACRxpB,QACAV,KAAKkqB,OAASA,EAElB,YACI,OAAOlqB,KAAKmqB,WAEhB,WAAWzoB,GACP,MAAMa,EAAe7B,MAAMW,WAAWK,GAEtC,OADCa,EAAatB,QAAUS,EAAWC,KAAK3B,KAAKkqB,QACtC3nB,EAEX,WACI,MAAM,SAAE4I,EAAQ,YAAEC,EAAW,OAAE8e,GAAWlqB,KAC1C,GAAImL,EACA,MAAMC,EAGV,OADApL,KAAKsL,iBACE4e,EAEX,KAAKtpB,GACDF,MAAMiB,KAAM3B,KAAKkqB,OAAStpB,M,6BCvBlC,4DAGO,SAASwpB,EAAKjF,GACjB,OAAOA,GAAS,EACV,IAAM,IACN,YAAQ,CAACxlB,EAAQ+B,KACf,IAAI2oB,EAAO,EACX1qB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,MAC3CypB,GAAQlF,IACVzjB,EAAWC,KAAKf,GACZukB,GAASkF,GACT3oB,EAAWD,mB,6BCZnC,2DAGO,MAAM6oB,EAAwB,CACjCC,SAAS,EACTC,UAAU,GAEP,SAASvM,EAASwM,GAAkB,QAAEF,EAAO,SAAEC,GAAaF,GAC/D,OAAO,YAAQ,CAAC3qB,EAAQ+B,KACpB,IAAIknB,GAAW,EACX8B,EAAY,KACZC,EAAY,KAChB,MAAMC,EAAiB,KACnBD,SAAsDA,EAAU7pB,cAChE6pB,EAAY,KACZH,GAAYK,KAEV5M,EAAYrd,GAAW+pB,EAAY,YAAKF,EAAiB7pB,IAAQQ,UAAU,IAAI,IAAmBM,EAAYkpB,OAAgB3hB,EAAW2hB,IACzIC,EAAO,KACLjC,IACAlnB,EAAWC,KAAK+oB,GAChBzM,EAASyM,IAEb9B,GAAW,EACX8B,EAAY,MAEhB/qB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjDgoB,GAAW,EACX8B,EAAY9pB,GACX+pB,IAAcJ,EAAUM,IAAS5M,EAASrd,W,6BC7BvD,8CACO,SAASkqB,EAAYC,EAAiB1hB,GACzC,OAAOA,EAAiB,YAAU,IAAM0hB,EAAiB1hB,GAAkB,YAAU,IAAM0hB,K,6BCF/F,oDAEO,SAASC,EAAOC,GACnB,OAAO,YAAQ,CAACtrB,EAAQ+B,KACpB,IAAIknB,GAAW,EACXsC,EAAY,KAChBvrB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjDgoB,GAAW,EACXsC,EAAYtqB,KAEhB,MAAMwR,EAAO,KACT,GAAIwW,EAAU,CACVA,GAAW,EACX,MAAMhoB,EAAQsqB,EACdA,EAAY,KACZxpB,EAAWC,KAAKf,KAGxBqqB,EAAS7pB,UAAU,IAAI,IAAmBM,EAAY0Q,OAAMnJ,EAAWmJ,Q,6BClB/E,oDAEO,SAAS+Y,EAAKhG,GACjB,OAAO,YAAQ,CAACxlB,EAAQ+B,KACpB,IAAI2oB,EAAO,EACX1qB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,GAAWukB,IAAUkF,EAAO3oB,EAAWC,KAAKf,GAASypB,U,6BCLlH,2DAGO,SAASe,EAAW9X,GACvB,OAAO,YAAQ,CAAC3T,EAAQ+B,KACpB,IAEI2pB,EAFAhD,EAAW,KACXiD,GAAY,EAEhBjD,EAAW1oB,EAAOyB,UAAU,IAAI,IAAmBM,OAAYuH,EAAYhJ,IACvEorB,EAAgB,YAAK/X,EAASrT,EAAKmrB,EAAW9X,EAAX8X,CAAqBzrB,KACpD0oB,GACAA,EAASvnB,cACTunB,EAAW,KACXgD,EAAcjqB,UAAUM,IAGxB4pB,GAAY,KAGhBA,IACAjD,EAASvnB,cACTunB,EAAW,KACXgD,EAAcjqB,UAAUM,Q,6BCtBpC,4DAGO,SAAS6pB,EAAaC,EAAStkB,EAAY,KAC9C,OAAO,YAAQ,CAACvH,EAAQ+B,KACpB,IAAIknB,GAAW,EACXsC,EAAY,KACZO,EAAuB,KAC3B,MAAMC,EAAgB,KAClB9C,GAAW,EACX,MAAMhoB,EAAQsqB,EACdA,EAAY,KACZxpB,EAAWC,KAAKf,IAEpBjB,EAAOyB,UAAU,IAAI,IAAmBM,EAAad,IACjD6qB,SAA4EA,EAAqB3qB,cACjG8nB,GAAW,EACXsC,EAAYtqB,EACZc,EAAWG,IAAK4pB,EAAuBvkB,EAAUI,SAAS,KACtDmkB,EAAuB,KACvBC,KACDF,UACJviB,EAAW,KACV2f,GAAY8C,IACZhqB,EAAWD,kB,6BCxBvB,8CACO,SAASkqB,EAAUnjB,EAASa,GAC/B,MAA8B,mBAAnBA,EACA,YAASb,EAASa,EAAgB,GAEtC,YAASb,EAAS,K,6BCL7B,sDAEO,SAASojB,EAAIC,EAAWC,EAAa,IAAOC,EAAc,KAC7D,OAAO,YAAM,IAAMF,IAAcC,EAAaC,K,+FCD3C,SAAS5D,IACZ,OAAO,YAAQ,CAACxoB,EAAQ+B,KACpB,IAAIsqB,EAAa,KACjBrsB,EAAOssB,YACP,MAAMC,EAAa,IAAI9rB,EAAA,EAAmBsB,OAAYuH,OAAWA,OAAWA,EAAW,KACnF,IAAKtJ,GAAUA,EAAOssB,WAAa,GAAK,IAAMtsB,EAAOssB,UAEjD,YADAD,EAAa,MAGjB,MAAMG,EAAmBxsB,EAAOysB,YAC1BC,EAAOL,EACbA,EAAa,MACTG,GAAsBE,GAAQF,IAAqBE,GACnDF,EAAiBrrB,cAErBY,EAAWZ,gBAEfnB,EAAOyB,UAAU8qB,GACZA,EAAWjrB,SACZ+qB,EAAarsB,EAAO2sB,aCjBzB,MAAM,UAA8BnrB,EAAA,EACvC,YAAYxB,EAAQ4sB,GAChB7rB,QACAV,KAAKL,OAASA,EACdK,KAAKusB,eAAiBA,EACtBvsB,KAAKwsB,SAAW,KAChBxsB,KAAKisB,UAAY,EACjBjsB,KAAKosB,YAAc,KAEvB,WAAW1qB,GACP,OAAO1B,KAAKysB,aAAarrB,UAAUM,GAEvC,aACI,MAAM2J,EAAUrL,KAAKwsB,SAIrB,OAHKnhB,IAAWA,EAAQpJ,YACpBjC,KAAKwsB,SAAWxsB,KAAKusB,kBAElBvsB,KAAKwsB,SAEhB,YACIxsB,KAAKisB,UAAY,EACjB,MAAM,YAAEG,GAAgBpsB,KACxBA,KAAKwsB,SAAWxsB,KAAKosB,YAAc,KACnCA,SAA0DA,EAAYtrB,cAE1E,UACI,IAAIkrB,EAAahsB,KAAKosB,YACtB,IAAKJ,EAAY,CACbA,EAAahsB,KAAKosB,YAAc,IAAI/kB,EAAA,EACpC,MAAMgE,EAAUrL,KAAKysB,aACrBT,EAAWnqB,IAAI7B,KAAKL,OAAOyB,UAAU,IAAIhB,EAAA,EAAmBiL,OAASpC,EAAYhJ,IAC7ED,KAAK0sB,YACLrhB,EAAQnL,MAAMD,IACf,KACCD,KAAK0sB,YACLrhB,EAAQ5J,YACT,IAAMzB,KAAK0sB,eACVV,EAAW/qB,SACXjB,KAAKosB,YAAc,KACnBJ,EAAa3kB,EAAA,EAAa5C,OAGlC,OAAOunB,EAEX,WACI,OAAO,IAAsBhsB,O,YC9CrC,SAAS2sB,IACL,OAAO,IAAIC,EAAA,EAER,SAASC,IACZ,OAAQltB,GAAWwoB,ICLhB,SAAmB2E,EAAyBxZ,GAC/C,MAAMiZ,EAAoD,mBAA5BO,EAAyCA,EAA0B,IAAMA,EACvG,MAAwB,mBAAbxZ,EACA,YAAQ,CAAC3T,EAAQ+B,KACpB,MAAM2J,EAAUkhB,IAChBjZ,EAASjI,GAASjK,UAAUM,GAAYG,IAAIlC,EAAOyB,UAAUiK,MAG7D1L,IACJ,MAAMotB,EAAc,IAAI,EAAsBptB,EAAQ4sB,GAMtD,OALI,YAAQ5sB,KACRotB,EAAYntB,KAAOD,EAAOC,MAE9BmtB,EAAYptB,OAASA,EACrBotB,EAAYR,eAAiBA,EACtBQ,GDVmBC,CAAUL,EAAVK,CAA+BrtB,M,mGEL1D,SAAS,KAAOstB,GACnB,OAAO,YAAQ,CAACttB,EAAQ+B,MCArB,YAAgBurB,GACnB,IAAI5jB,OAAiBJ,EAIrB,MAH2C,mBAAhCgkB,EAAQA,EAAQxqB,OAAS,KAChC4G,EAAiB4jB,EAAQ5S,OAEtB,IAAIlZ,EAAA,EAAYO,IACnB,MAAMunB,EAAUgE,EAAQ/pB,IAAI,IAAM,IAC5BgqB,EAAYD,EAAQ/pB,IAAI,KAAM,GAC9BX,EAAe,IAAI8E,EAAA,EACnB8lB,EAAU,KACZ,GAAIlE,EAAQhO,MAAOtQ,GAAWA,EAAOlI,OAAS,GAAI,CAC9C,IAAI+C,EAASyjB,EAAQ/lB,IAAKyH,GAAWA,EAAOE,SAC5C,GAAIxB,EACA,IACI7D,EAAS6D,KAAkB7D,GAE/B,MAAOvF,GAEH,YADAyB,EAAWxB,MAAMD,GAIzByB,EAAWC,KAAK6D,GACZyjB,EAAQhN,KAAK,CAACtR,EAAQxH,IAAwB,IAAlBwH,EAAOlI,QAAgByqB,EAAU/pB,KAC7DzB,EAAWD,aAIvB,IAAK,IAAI0B,EAAI,GAAIzB,EAAWT,QAAUkC,EAAI8pB,EAAQxqB,OAAQU,IAAK,CAC3D,MAAMxD,EAAS,OAAAoI,EAAA,GAAKklB,EAAQ9pB,IAC5BZ,EAAaV,IAAIlC,EAAOyB,UAAU,CAC9BO,KAAOf,IACHqoB,EAAQ9lB,GAAGgB,KAAKvD,GAChBusB,KAEJjtB,MAAQD,GAAQyB,EAAWxB,MAAMD,GACjCwB,SAAU,KACNyrB,EAAU/pB,IAAK,EACW,IAAtB8lB,EAAQ9lB,GAAGV,QACXf,EAAWD,eAK3B,OAAOc,KD1CP6qB,CAAUztB,KAAWstB,GAAS7rB,UAAUM,KAGzC,SAAS2rB,KAAWC,GACvB,OAAO,KAAOA,K,2FERlB,MAAM,QAAE1pB,GAAYD,M,mBCMb,SAAS4pB,KAASrgB,GACrB,IAAI9C,EAAaC,IACbnD,OAAY+B,EAQhB,OAPI,OAAAN,EAAA,GAAYuE,EAAKA,EAAKzK,OAAS,MAC/ByE,EAAYgG,EAAKmN,OAEgB,iBAA1BnN,EAAKA,EAAKzK,OAAS,KAC1B2H,EAAa8C,EAAKmN,QAEtBnN,EDdG,SAAwBA,GAC3B,OAAuB,IAAhBA,EAAKzK,QAAgBmB,EAAQsJ,EAAK,IAAMA,EAAK,GAAKA,ECalDsgB,CAAetgB,IACTzK,OAGS,IAAhByK,EAAKzK,OAEC,OAAAsF,EAAA,GAAKmF,EAAK,IAEV,OAAAI,EAAA,GAASlD,EAAT,CAAqB,OAAAR,EAAA,GAAUsD,EAAMhG,IALzC,M,6FCdL,SAAS,EAAMwG,EAAOxG,EAAY,KACrC,OAAO,YAAQ,CAACvH,EAAQ+B,KACpB,MAAM+rB,GCNc7sB,EDMgB8M,aCLhBV,OAAS0gB,MAAM9sB,GADpC,IAAqBA,EDOpB,IAAI2I,GAAa,EACbmB,EAAS,EACTijB,EAAqBF,EAAkB,GAAK,KAChD,MAAMjkB,EAAgB,IAAMD,IAAemB,KAAYijB,aAA+D,EAASA,EAAmBlrB,SAAWf,EAAWD,WA+BxK,OA9BIgsB,IACA/iB,IACAhJ,EAAWG,IAAIqF,EAAUI,SAAS,KAE9B,GADAoD,IACIijB,EAAoB,CACpB,MAAM7S,EAAS6S,EACfA,EAAqB,KACrB,IAAK,MAAM/sB,KAASka,EAChBpZ,EAAWC,KAAKf,GAGxB4I,MACAkE,EAAQxG,EAAU4F,SAE1BnN,EAAOyB,UAAU,IAAIhB,EAAA,EAAmBsB,EAAad,IAC7C6sB,EACAE,EAAqBA,EAAmBxpB,KAAKvD,GAASc,EAAWC,KAAKf,IAGtE8J,IACAhJ,EAAWG,IAAIqF,EAAUI,SAAS,KAC9BoD,IACAhJ,EAAWC,KAAKf,GAChB4I,KACDkE,WAERzE,EAAW,KACVM,GAAa,EACbC,OAEG,KACHmkB,EAAqB","file":"assets/javascripts/vendor.77e55a48.min.js","sourcesContent":["export function hasLift(source) {\n return typeof (source === null || source === void 0 ? void 0 : source.lift) === 'function';\n}\nexport function operate(init) {\n return (source) => {\n if (hasLift(source)) {\n return source.lift(function (liftedSource) {\n try {\n return init(liftedSource, this);\n }\n catch (err) {\n this.error(err);\n }\n });\n }\n throw new TypeError('Unable to lift unknown Observable type');\n };\n}\n//# sourceMappingURL=lift.js.map","import { Subscriber } from '../Subscriber';\nexport class OperatorSubscriber extends Subscriber {\n constructor(destination, onNext, onError, onComplete, onUnsubscribe) {\n super(destination);\n this.onUnsubscribe = onUnsubscribe;\n if (onNext) {\n this._next = function (value) {\n try {\n onNext(value);\n }\n catch (err) {\n this.error(err);\n }\n };\n }\n if (onError) {\n this._error = function (err) {\n try {\n onError(err);\n }\n catch (err) {\n this.destination.error(err);\n }\n this.unsubscribe();\n };\n }\n if (onComplete) {\n this._complete = function () {\n try {\n onComplete();\n }\n catch (err) {\n this.destination.error(err);\n }\n this.unsubscribe();\n };\n }\n }\n unsubscribe() {\n var _a;\n !this.closed && ((_a = this.onUnsubscribe) === null || _a === void 0 ? void 0 : _a.call(this));\n super.unsubscribe();\n }\n}\n//# sourceMappingURL=OperatorSubscriber.js.map","import { SafeSubscriber, Subscriber } from './Subscriber';\nimport { isSubscription } from './Subscription';\nimport { observable as Symbol_observable } from './symbol/observable';\nimport { pipeFromArray } from './util/pipe';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nexport class Observable {\n constructor(subscribe) {\n if (subscribe) {\n this._subscribe = subscribe;\n }\n }\n lift(operator) {\n const observable = new Observable();\n observable.source = this;\n observable.operator = operator;\n return observable;\n }\n subscribe(observerOrNext, error, complete) {\n const subscriber = isSubscriber(observerOrNext) ? observerOrNext : new SafeSubscriber(observerOrNext, error, complete);\n const { operator, source } = this;\n subscriber.add(operator\n ? operator.call(subscriber, source)\n : source || config.useDeprecatedSynchronousErrorHandling\n ? this._subscribe(subscriber)\n : this._trySubscribe(subscriber));\n return subscriber;\n }\n _trySubscribe(sink) {\n try {\n return this._subscribe(sink);\n }\n catch (err) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n throw err;\n }\n else {\n canReportError(sink) ? sink.error(err) : reportUnhandledError(err);\n }\n }\n }\n forEach(next, promiseCtor) {\n promiseCtor = getPromiseCtor(promiseCtor);\n return new promiseCtor((resolve, reject) => {\n let subscription;\n subscription = this.subscribe((value) => {\n try {\n next(value);\n }\n catch (err) {\n reject(err);\n subscription === null || subscription === void 0 ? void 0 : subscription.unsubscribe();\n }\n }, reject, resolve);\n });\n }\n _subscribe(subscriber) {\n var _a;\n return (_a = this.source) === null || _a === void 0 ? void 0 : _a.subscribe(subscriber);\n }\n [Symbol_observable]() {\n return this;\n }\n pipe(...operations) {\n return operations.length ? pipeFromArray(operations)(this) : this;\n }\n toPromise(promiseCtor) {\n promiseCtor = getPromiseCtor(promiseCtor);\n return new promiseCtor((resolve, reject) => {\n let value;\n this.subscribe((x) => (value = x), (err) => reject(err), () => resolve(value));\n });\n }\n}\nObservable.create = (subscribe) => {\n return new Observable(subscribe);\n};\nfunction getPromiseCtor(promiseCtor) {\n var _a;\n return (_a = promiseCtor !== null && promiseCtor !== void 0 ? promiseCtor : config.Promise) !== null && _a !== void 0 ? _a : Promise;\n}\nexport function canReportError(subscriber) {\n while (subscriber) {\n const { closed, destination, isStopped } = subscriber;\n if (closed || isStopped) {\n return false;\n }\n subscriber = destination && destination instanceof Subscriber ? destination : null;\n }\n return true;\n}\nfunction isObserver(value) {\n return value && typeof value.next === 'function' && typeof value.error === 'function' && typeof value.complete === 'function';\n}\nfunction isSubscriber(value) {\n return (value && value instanceof Subscriber) || (isObserver(value) && isSubscription(value));\n}\n//# sourceMappingURL=Observable.js.map","import { createErrorClass } from './createErrorClass';\nexport const UnsubscriptionError = createErrorClass((_super) => function UnsubscriptionError(errors) {\n _super(this);\n this.message = errors\n ? `${errors.length} errors occurred during unsubscription:\n${errors.map((err, i) => `${i + 1}) ${err.toString()}`).join('\\n ')}`\n : '';\n this.name = 'UnsubscriptionError';\n this.errors = errors;\n});\n//# sourceMappingURL=UnsubscriptionError.js.map","import { isFunction } from './util/isFunction';\nimport { UnsubscriptionError } from './util/UnsubscriptionError';\nimport { arrRemove } from './util/arrRemove';\nexport class Subscription {\n constructor(initialTeardown) {\n this.initialTeardown = initialTeardown;\n this.closed = false;\n this._parentage = null;\n this._teardowns = null;\n }\n unsubscribe() {\n let errors;\n if (!this.closed) {\n this.closed = true;\n const { _parentage } = this;\n if (Array.isArray(_parentage)) {\n for (const parent of _parentage) {\n parent.remove(this);\n }\n }\n else {\n _parentage === null || _parentage === void 0 ? void 0 : _parentage.remove(this);\n }\n const { initialTeardown } = this;\n if (isFunction(initialTeardown)) {\n try {\n initialTeardown();\n }\n catch (e) {\n errors = e instanceof UnsubscriptionError ? e.errors : [e];\n }\n }\n const { _teardowns } = this;\n if (_teardowns) {\n this._teardowns = null;\n for (const teardown of _teardowns) {\n try {\n execTeardown(teardown);\n }\n catch (err) {\n errors = errors !== null && errors !== void 0 ? errors : [];\n if (err instanceof UnsubscriptionError) {\n errors = [...errors, ...err.errors];\n }\n else {\n errors.push(err);\n }\n }\n }\n }\n if (errors) {\n throw new UnsubscriptionError(errors);\n }\n }\n }\n add(teardown) {\n var _a;\n if (teardown && teardown !== this) {\n if (this.closed) {\n execTeardown(teardown);\n }\n else {\n if (teardown instanceof Subscription) {\n if (teardown.closed || teardown._hasParent(this)) {\n return;\n }\n teardown._addParent(this);\n }\n (this._teardowns = (_a = this._teardowns) !== null && _a !== void 0 ? _a : []).push(teardown);\n }\n }\n }\n _hasParent(parent) {\n const { _parentage } = this;\n return _parentage === parent || (Array.isArray(_parentage) && _parentage.includes(parent));\n }\n _addParent(parent) {\n const { _parentage } = this;\n this._parentage = Array.isArray(_parentage) ? (_parentage.push(parent), _parentage) : _parentage ? [_parentage, parent] : parent;\n }\n _removeParent(parent) {\n const { _parentage } = this;\n if (_parentage === parent) {\n this._parentage = null;\n }\n else if (Array.isArray(_parentage)) {\n arrRemove(_parentage, parent);\n }\n }\n remove(teardown) {\n const { _teardowns } = this;\n _teardowns && arrRemove(_teardowns, teardown);\n if (teardown instanceof Subscription) {\n teardown._removeParent(this);\n }\n }\n}\nSubscription.EMPTY = (function (empty) {\n empty.closed = true;\n return empty;\n})(new Subscription());\nexport const EMPTY_SUBSCRIPTION = Subscription.EMPTY;\nexport function isSubscription(value) {\n return (value instanceof Subscription ||\n (value &&\n 'closed' in value &&\n typeof value.remove === 'function' &&\n typeof value.add === 'function' &&\n typeof value.unsubscribe === 'function'));\n}\nfunction execTeardown(teardown) {\n if (typeof teardown === 'function') {\n teardown();\n }\n else {\n teardown.unsubscribe();\n }\n}\n//# sourceMappingURL=Subscription.js.map","export function getSymbolIterator() {\n if (typeof Symbol !== 'function' || !Symbol.iterator) {\n return '@@iterator';\n }\n return Symbol.iterator;\n}\nexport const iterator = getSymbolIterator();\nexport const $$iterator = iterator;\n//# sourceMappingURL=iterator.js.map","export function isPromise(value) {\n return !!value && typeof value.subscribe !== 'function' && typeof value.then === 'function';\n}\n//# sourceMappingURL=isPromise.js.map","/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","import { __asyncValues, __awaiter } from \"tslib\";\nexport function subscribeToAsyncIterable(asyncIterable) {\n return (subscriber) => {\n process(asyncIterable, subscriber).catch(err => subscriber.error(err));\n };\n}\nfunction process(asyncIterable, subscriber) {\n var asyncIterable_1, asyncIterable_1_1;\n var e_1, _a;\n return __awaiter(this, void 0, void 0, function* () {\n try {\n for (asyncIterable_1 = __asyncValues(asyncIterable); asyncIterable_1_1 = yield asyncIterable_1.next(), !asyncIterable_1_1.done;) {\n const value = asyncIterable_1_1.value;\n subscriber.next(value);\n }\n }\n catch (e_1_1) { e_1 = { error: e_1_1 }; }\n finally {\n try {\n if (asyncIterable_1_1 && !asyncIterable_1_1.done && (_a = asyncIterable_1.return)) yield _a.call(asyncIterable_1);\n }\n finally { if (e_1) throw e_1.error; }\n }\n subscriber.complete();\n });\n}\n//# sourceMappingURL=subscribeToAsyncIterable.js.map","import { scheduleObservable } from './scheduleObservable';\nimport { schedulePromise } from './schedulePromise';\nimport { scheduleArray } from './scheduleArray';\nimport { scheduleIterable } from './scheduleIterable';\nimport { isInteropObservable } from '../util/isInteropObservable';\nimport { isPromise } from '../util/isPromise';\nimport { isArrayLike } from '../util/isArrayLike';\nimport { isIterable } from '../util/isIterable';\nimport { scheduleAsyncIterable } from './scheduleAsyncIterable';\nexport function scheduled(input, scheduler) {\n if (input != null) {\n if (isInteropObservable(input)) {\n return scheduleObservable(input, scheduler);\n }\n else if (isPromise(input)) {\n return schedulePromise(input, scheduler);\n }\n else if (isArrayLike(input)) {\n return scheduleArray(input, scheduler);\n }\n else if (isIterable(input) || typeof input === 'string') {\n return scheduleIterable(input, scheduler);\n }\n else if (Symbol && Symbol.asyncIterator && typeof input[Symbol.asyncIterator] === 'function') {\n return scheduleAsyncIterable(input, scheduler);\n }\n }\n throw new TypeError((input !== null && typeof input || input) + ' is not observable');\n}\n//# sourceMappingURL=scheduled.js.map","import { observable as Symbol_observable } from '../symbol/observable';\nexport function isInteropObservable(input) {\n return input && typeof input[Symbol_observable] === 'function';\n}\n//# sourceMappingURL=isInteropObservable.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nimport { observable as Symbol_observable } from '../symbol/observable';\nexport function scheduleObservable(input, scheduler) {\n return new Observable(subscriber => {\n const sub = new Subscription();\n sub.add(scheduler.schedule(() => {\n const observable = input[Symbol_observable]();\n sub.add(observable.subscribe({\n next(value) { sub.add(scheduler.schedule(() => subscriber.next(value))); },\n error(err) { sub.add(scheduler.schedule(() => subscriber.error(err))); },\n complete() { sub.add(scheduler.schedule(() => subscriber.complete())); },\n }));\n }));\n return sub;\n });\n}\n//# sourceMappingURL=scheduleObservable.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nexport function schedulePromise(input, scheduler) {\n return new Observable(subscriber => {\n const sub = new Subscription();\n sub.add(scheduler.schedule(() => input.then(value => {\n sub.add(scheduler.schedule(() => {\n subscriber.next(value);\n sub.add(scheduler.schedule(() => subscriber.complete()));\n }));\n }, err => {\n sub.add(scheduler.schedule(() => subscriber.error(err)));\n })));\n return sub;\n });\n}\n//# sourceMappingURL=schedulePromise.js.map","import { iterator as Symbol_iterator } from '../symbol/iterator';\nexport function isIterable(input) {\n return input && typeof input[Symbol_iterator] === 'function';\n}\n//# sourceMappingURL=isIterable.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nimport { iterator as Symbol_iterator } from '../symbol/iterator';\nexport function scheduleIterable(input, scheduler) {\n if (!input) {\n throw new Error('Iterable cannot be null');\n }\n return new Observable(subscriber => {\n const sub = new Subscription();\n let iterator;\n sub.add(() => {\n if (iterator && typeof iterator.return === 'function') {\n iterator.return();\n }\n });\n sub.add(scheduler.schedule(() => {\n iterator = input[Symbol_iterator]();\n sub.add(scheduler.schedule(function () {\n if (subscriber.closed) {\n return;\n }\n let value;\n let done;\n try {\n const result = iterator.next();\n value = result.value;\n done = result.done;\n }\n catch (err) {\n subscriber.error(err);\n return;\n }\n if (done) {\n subscriber.complete();\n }\n else {\n subscriber.next(value);\n this.schedule();\n }\n }));\n }));\n return sub;\n });\n}\n//# sourceMappingURL=scheduleIterable.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nexport function scheduleAsyncIterable(input, scheduler) {\n if (!input) {\n throw new Error('Iterable cannot be null');\n }\n return new Observable(subscriber => {\n const sub = new Subscription();\n sub.add(scheduler.schedule(() => {\n const iterator = input[Symbol.asyncIterator]();\n sub.add(scheduler.schedule(function () {\n iterator.next().then(result => {\n if (result.done) {\n subscriber.complete();\n }\n else {\n subscriber.next(result.value);\n this.schedule();\n }\n });\n }));\n }));\n return sub;\n });\n}\n//# sourceMappingURL=scheduleAsyncIterable.js.map","import { subscribeToArray } from '../util/subscribeToArray';\nimport { subscribeToPromise } from '../util/subscribeToPromise';\nimport { subscribeToIterable } from '../util/subscribeToIterable';\nimport { subscribeToObservable } from '../util/subscribeToObservable';\nimport { isArrayLike } from '../util/isArrayLike';\nimport { isPromise } from '../util/isPromise';\nimport { isObject } from '../util/isObject';\nimport { iterator as Symbol_iterator } from '../symbol/iterator';\nimport { observable as Symbol_observable } from '../symbol/observable';\nimport { subscribeToAsyncIterable } from '../util/subscribeToAsyncIterable';\nimport { Observable } from '../Observable';\nimport { scheduled } from '../scheduled/scheduled';\nexport function from(input, scheduler) {\n if (!scheduler) {\n if (input instanceof Observable) {\n return input;\n }\n return new Observable(subscribeTo(input));\n }\n else {\n return scheduled(input, scheduler);\n }\n}\nfunction subscribeTo(result) {\n if (result && typeof result[Symbol_observable] === 'function') {\n return subscribeToObservable(result);\n }\n else if (isArrayLike(result)) {\n return subscribeToArray(result);\n }\n else if (isPromise(result)) {\n return subscribeToPromise(result);\n }\n else if (result && typeof result[Symbol_iterator] === 'function') {\n return subscribeToIterable(result);\n }\n else if (Symbol && Symbol.asyncIterator &&\n !!result && typeof result[Symbol.asyncIterator] === 'function') {\n return subscribeToAsyncIterable(result);\n }\n else {\n const value = isObject(result) ? 'an invalid object' : `'${result}'`;\n const msg = `You provided ${value} where a stream was expected.`\n + ' You can provide an Observable, Promise, Array, AsyncIterable, or Iterable.';\n throw new TypeError(msg);\n }\n}\n;\n//# sourceMappingURL=from.js.map","import { observable as Symbol_observable } from '../symbol/observable';\nexport const subscribeToObservable = (obj) => (subscriber) => {\n const obs = obj[Symbol_observable]();\n if (typeof obs.subscribe !== 'function') {\n throw new TypeError('Provided object does not correctly implement Symbol.observable');\n }\n else {\n return obs.subscribe(subscriber);\n }\n};\n//# sourceMappingURL=subscribeToObservable.js.map","import { reportUnhandledError } from './reportUnhandledError';\nexport const subscribeToPromise = (promise) => (subscriber) => {\n promise.then((value) => {\n if (!subscriber.closed) {\n subscriber.next(value);\n subscriber.complete();\n }\n }, (err) => subscriber.error(err))\n .then(null, reportUnhandledError);\n return subscriber;\n};\n//# sourceMappingURL=subscribeToPromise.js.map","import { iterator as Symbol_iterator } from '../symbol/iterator';\nexport const subscribeToIterable = (iterable) => (subscriber) => {\n const iterator = iterable[Symbol_iterator]();\n do {\n let item;\n try {\n item = iterator.next();\n }\n catch (err) {\n subscriber.error(err);\n return;\n }\n if (item.done) {\n subscriber.complete();\n break;\n }\n subscriber.next(item.value);\n if (subscriber.closed) {\n break;\n }\n } while (true);\n if (typeof iterator.return === 'function') {\n subscriber.add(() => {\n if (iterator.return) {\n iterator.return();\n }\n });\n }\n return subscriber;\n};\n//# sourceMappingURL=subscribeToIterable.js.map","export function isObject(x) {\n return x !== null && typeof x === 'object';\n}\n//# sourceMappingURL=isObject.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function map(project, thisArg) {\n return operate((source, subscriber) => {\n let index = 0;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n subscriber.next(project.call(thisArg, value, index++));\n }));\n });\n}\n//# sourceMappingURL=map.js.map","export function identity(x) {\n return x;\n}\n//# sourceMappingURL=identity.js.map","export const observable = (() => typeof Symbol === 'function' && Symbol.observable || '@@observable')();\n//# sourceMappingURL=observable.js.map","export function isScheduler(value) {\n return value && typeof value.schedule === 'function';\n}\n//# sourceMappingURL=isScheduler.js.map","export function arrRemove(arr, item) {\n if (arr) {\n const index = arr.indexOf(item);\n 0 <= index && arr.splice(index, 1);\n }\n}\n//# sourceMappingURL=arrRemove.js.map","export const config = {\n onUnhandledError: null,\n Promise: undefined,\n useDeprecatedSynchronousErrorHandling: false,\n useDeprecatedNextContext: false,\n};\n//# sourceMappingURL=config.js.map","export function noop() { }\n//# sourceMappingURL=noop.js.map","import { from } from '../observable/from';\nimport { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function switchMap(project, resultSelector) {\n return operate((source, subscriber) => {\n let innerSubscriber = null;\n let index = 0;\n let isComplete = false;\n const checkComplete = () => isComplete && !innerSubscriber && subscriber.complete();\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n innerSubscriber === null || innerSubscriber === void 0 ? void 0 : innerSubscriber.unsubscribe();\n let innerIndex = 0;\n let outerIndex = index++;\n from(project(value, outerIndex)).subscribe((innerSubscriber = new OperatorSubscriber(subscriber, (innerValue) => subscriber.next(resultSelector ? resultSelector(value, innerValue, outerIndex, innerIndex++) : innerValue), undefined, () => {\n innerSubscriber = null;\n checkComplete();\n })));\n }, undefined, () => {\n isComplete = true;\n checkComplete();\n }));\n });\n}\n//# sourceMappingURL=switchMap.js.map","export function isFunction(x) {\n return typeof x === 'function';\n}\n//# sourceMappingURL=isFunction.js.map","import { Observable } from '../Observable';\nimport { subscribeToArray } from '../util/subscribeToArray';\nimport { scheduleArray } from '../scheduled/scheduleArray';\nexport function fromArray(input, scheduler) {\n if (!scheduler) {\n return new Observable(subscribeToArray(input));\n }\n else {\n return scheduleArray(input, scheduler);\n }\n}\n//# sourceMappingURL=fromArray.js.map","import { isFunction } from './util/isFunction';\nimport { isSubscription, Subscription } from './Subscription';\nimport { config } from './config';\nimport { reportUnhandledError } from './util/reportUnhandledError';\nimport { noop } from './util/noop';\nexport class Subscriber extends Subscription {\n constructor(destination) {\n super();\n this.isStopped = false;\n if (destination) {\n this.destination = destination;\n if (isSubscription(destination)) {\n destination.add(this);\n }\n }\n else {\n this.destination = EMPTY_OBSERVER;\n }\n }\n static create(next, error, complete) {\n return new SafeSubscriber(next, error, complete);\n }\n next(value) {\n if (!this.isStopped) {\n this._next(value);\n }\n }\n error(err) {\n if (!this.isStopped) {\n this.isStopped = true;\n this._error(err);\n }\n }\n complete() {\n if (!this.isStopped) {\n this.isStopped = true;\n this._complete();\n }\n }\n unsubscribe() {\n if (!this.closed) {\n this.isStopped = true;\n super.unsubscribe();\n }\n }\n _next(value) {\n this.destination.next(value);\n }\n _error(err) {\n this.destination.error(err);\n this.unsubscribe();\n }\n _complete() {\n this.destination.complete();\n this.unsubscribe();\n }\n}\nexport class SafeSubscriber extends Subscriber {\n constructor(observerOrNext, error, complete) {\n super();\n this.destination = EMPTY_OBSERVER;\n if ((observerOrNext || error || complete) && observerOrNext !== EMPTY_OBSERVER) {\n let next;\n if (isFunction(observerOrNext)) {\n next = observerOrNext;\n }\n else if (observerOrNext) {\n ({ next, error, complete } = observerOrNext);\n let context;\n if (this && config.useDeprecatedNextContext) {\n context = Object.create(observerOrNext);\n context.unsubscribe = () => this.unsubscribe();\n }\n else {\n context = observerOrNext;\n }\n next = next === null || next === void 0 ? void 0 : next.bind(context);\n error = error === null || error === void 0 ? void 0 : error.bind(context);\n complete = complete === null || complete === void 0 ? void 0 : complete.bind(context);\n }\n this.destination = {\n next: next || noop,\n error: error || defaultErrorHandler,\n complete: complete || noop,\n };\n }\n }\n}\nfunction defaultErrorHandler(err) {\n if (config.useDeprecatedSynchronousErrorHandling) {\n throw err;\n }\n reportUnhandledError(err);\n}\nexport const EMPTY_OBSERVER = {\n closed: true,\n next: noop,\n error: defaultErrorHandler,\n complete: noop,\n};\n//# sourceMappingURL=Subscriber.js.map","import { Observable } from '../Observable';\nexport const EMPTY = new Observable(subscriber => subscriber.complete());\nexport function empty(scheduler) {\n return scheduler ? emptyScheduled(scheduler) : EMPTY;\n}\nfunction emptyScheduled(scheduler) {\n return new Observable(subscriber => scheduler.schedule(() => subscriber.complete()));\n}\n//# sourceMappingURL=empty.js.map","import { map } from './map';\nimport { from } from '../observable/from';\nimport { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function mergeMap(project, resultSelector, concurrent = Infinity) {\n if (typeof resultSelector === 'function') {\n return (source) => source.pipe(mergeMap((a, i) => from(project(a, i)).pipe(map((b, ii) => resultSelector(a, b, i, ii))), concurrent));\n }\n else if (typeof resultSelector === 'number') {\n concurrent = resultSelector;\n }\n return operate((source, subscriber) => {\n let isComplete = false;\n let active = 0;\n let index = 0;\n let buffer = [];\n const checkComplete = () => isComplete && !active && subscriber.complete();\n const tryInnerSub = () => {\n while (active < concurrent && buffer.length > 0) {\n doInnerSub(buffer.shift());\n }\n };\n const doInnerSub = (value) => {\n active++;\n subscriber.add(from(project(value, index++)).subscribe(new OperatorSubscriber(subscriber, (innerValue) => subscriber.next(innerValue), undefined, () => {\n active--;\n buffer.length && tryInnerSub();\n checkComplete();\n })));\n };\n let outerSubs;\n outerSubs = source.subscribe(new OperatorSubscriber(subscriber, (value) => (active < concurrent ? doInnerSub(value) : buffer.push(value)), undefined, () => {\n isComplete = true;\n checkComplete();\n outerSubs === null || outerSubs === void 0 ? void 0 : outerSubs.unsubscribe();\n }));\n return () => {\n buffer = null;\n };\n });\n}\nexport const flatMap = mergeMap;\n//# sourceMappingURL=mergeMap.js.map","import { config } from '../config';\nexport function reportUnhandledError(err) {\n setTimeout(() => {\n const { onUnhandledError } = config;\n if (onUnhandledError) {\n onUnhandledError(err);\n }\n else {\n throw err;\n }\n });\n}\n//# sourceMappingURL=reportUnhandledError.js.map","export const isArrayLike = ((x) => x && typeof x.length === 'number' && typeof x !== 'function');\n//# sourceMappingURL=isArrayLike.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nexport function scheduleArray(input, scheduler) {\n return new Observable(subscriber => {\n const sub = new Subscription();\n let i = 0;\n sub.add(scheduler.schedule(function () {\n if (i === input.length) {\n subscriber.complete();\n return;\n }\n subscriber.next(input[i++]);\n if (!subscriber.closed) {\n sub.add(this.schedule());\n }\n }));\n return sub;\n });\n}\n//# sourceMappingURL=scheduleArray.js.map","import { createErrorClass } from './createErrorClass';\nexport const ObjectUnsubscribedError = createErrorClass((_super) => function ObjectUnsubscribedError() {\n _super(this);\n this.message = 'object unsubscribed';\n});\n//# sourceMappingURL=ObjectUnsubscribedError.js.map","import { Observable } from './Observable';\nimport { Subscription, EMPTY_SUBSCRIPTION } from './Subscription';\nimport { ObjectUnsubscribedError } from './util/ObjectUnsubscribedError';\nimport { arrRemove } from './util/arrRemove';\nexport class Subject extends Observable {\n constructor() {\n super();\n this.observers = [];\n this.closed = false;\n this.isStopped = false;\n this.hasError = false;\n this.thrownError = null;\n }\n lift(operator) {\n const subject = new AnonymousSubject(this, this);\n subject.operator = operator;\n return subject;\n }\n _throwIfClosed() {\n if (this.closed) {\n throw new ObjectUnsubscribedError();\n }\n }\n next(value) {\n this._throwIfClosed();\n if (!this.isStopped) {\n const copy = this.observers.slice();\n for (const observer of copy) {\n observer.next(value);\n }\n }\n }\n error(err) {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.hasError = this.isStopped = true;\n this.thrownError = err;\n const { observers } = this;\n while (observers.length) {\n observers.shift().error(err);\n }\n }\n }\n complete() {\n this._throwIfClosed();\n if (!this.isStopped) {\n this.isStopped = true;\n const { observers } = this;\n while (observers.length) {\n observers.shift().complete();\n }\n }\n }\n unsubscribe() {\n this.isStopped = this.closed = true;\n this.observers = null;\n }\n _trySubscribe(subscriber) {\n this._throwIfClosed();\n return super._trySubscribe(subscriber);\n }\n _subscribe(subscriber) {\n this._throwIfClosed();\n this._checkFinalizedStatuses(subscriber);\n return this._innerSubscribe(subscriber);\n }\n _innerSubscribe(subscriber) {\n const { hasError, isStopped, observers } = this;\n return hasError || isStopped\n ? EMPTY_SUBSCRIPTION\n : (observers.push(subscriber), new Subscription(() => arrRemove(this.observers, subscriber)));\n }\n _checkFinalizedStatuses(subscriber) {\n const { hasError, thrownError, isStopped } = this;\n if (hasError) {\n subscriber.error(thrownError);\n }\n else if (isStopped) {\n subscriber.complete();\n }\n }\n asObservable() {\n const observable = new Observable();\n observable.source = this;\n return observable;\n }\n}\nSubject.create = (destination, source) => {\n return new AnonymousSubject(destination, source);\n};\nexport class AnonymousSubject extends Subject {\n constructor(destination, source) {\n super();\n this.destination = destination;\n this.source = source;\n }\n next(value) {\n var _a, _b;\n (_b = (_a = this.destination) === null || _a === void 0 ? void 0 : _a.next) === null || _b === void 0 ? void 0 : _b.call(_a, value);\n }\n error(err) {\n var _a, _b;\n (_b = (_a = this.destination) === null || _a === void 0 ? void 0 : _a.error) === null || _b === void 0 ? void 0 : _b.call(_a, err);\n }\n complete() {\n var _a, _b;\n (_b = (_a = this.destination) === null || _a === void 0 ? void 0 : _a.complete) === null || _b === void 0 ? void 0 : _b.call(_a);\n }\n _subscribe(subscriber) {\n var _a, _b;\n return (_b = (_a = this.source) === null || _a === void 0 ? void 0 : _a.subscribe(subscriber)) !== null && _b !== void 0 ? _b : EMPTY_SUBSCRIPTION;\n }\n}\n//# sourceMappingURL=Subject.js.map","import { identity } from './identity';\nexport function pipe(...fns) {\n return pipeFromArray(fns);\n}\nexport function pipeFromArray(fns) {\n if (fns.length === 0) {\n return identity;\n }\n if (fns.length === 1) {\n return fns[0];\n }\n return function piped(input) {\n return fns.reduce((prev, fn) => fn(prev), input);\n };\n}\n//# sourceMappingURL=pipe.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function distinctUntilChanged(compare, keySelector) {\n compare = compare !== null && compare !== void 0 ? compare : defaultCompare;\n return operate((source, subscriber) => {\n let prev;\n let first = true;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n ((first && ((prev = value), 1)) || !compare(prev, (prev = keySelector ? keySelector(value) : value))) &&\n subscriber.next(value);\n first = false;\n }));\n });\n}\nfunction defaultCompare(a, b) {\n return a === b;\n}\n//# sourceMappingURL=distinctUntilChanged.js.map","export function createErrorClass(createImpl) {\n const _super = (instance) => {\n Error.call(instance);\n instance.name = instance.constructor.name;\n instance.stack = new Error().stack;\n };\n const ctorFunc = createImpl(_super);\n ctorFunc.prototype = Object.create(Error.prototype);\n ctorFunc.prototype.constructor = ctorFunc;\n return ctorFunc;\n}\n//# sourceMappingURL=createErrorClass.js.map","export const dateTimestampProvider = {\n now() {\n return (dateTimestampProvider.delegate || Date).now();\n },\n delegate: undefined,\n};\n//# sourceMappingURL=dateTimestampProvider.js.map","import { map } from \"../operators/map\";\nconst { isArray } = Array;\nfunction callOrApply(fn, args) {\n return isArray(args) ? fn(...args) : fn(args);\n}\nexport function mapOneOrManyArgs(fn) {\n return map(args => callOrApply(fn, args));\n}\n//# sourceMappingURL=mapOneOrManyArgs.js.map","export const subscribeToArray = (array) => (subscriber) => {\n for (let i = 0, len = array.length; i < len && !subscriber.closed; i++) {\n subscriber.next(array[i]);\n }\n subscriber.complete();\n};\n//# sourceMappingURL=subscribeToArray.js.map","import { mergeMap } from './mergeMap';\nimport { identity } from '../util/identity';\nexport function mergeAll(concurrent = Infinity) {\n return mergeMap(identity, concurrent);\n}\n//# sourceMappingURL=mergeAll.js.map","import { AsyncAction } from './AsyncAction';\nimport { AsyncScheduler } from './AsyncScheduler';\nexport const asyncScheduler = new AsyncScheduler(AsyncAction);\nexport const async = asyncScheduler;\n//# sourceMappingURL=async.js.map","import { Subscription } from '../Subscription';\nexport class Action extends Subscription {\n constructor(scheduler, work) {\n super();\n }\n schedule(state, delay = 0) {\n return this;\n }\n}\n//# sourceMappingURL=Action.js.map","export const intervalProvider = {\n setInterval(...args) {\n const { delegate } = intervalProvider;\n return ((delegate === null || delegate === void 0 ? void 0 : delegate.setInterval) || setInterval)(...args);\n },\n clearInterval(handle) {\n const { delegate } = intervalProvider;\n return ((delegate === null || delegate === void 0 ? void 0 : delegate.clearInterval) || clearInterval)(handle);\n },\n delegate: undefined,\n};\n//# sourceMappingURL=intervalProvider.js.map","import { Action } from './Action';\nimport { intervalProvider } from './intervalProvider';\nimport { arrRemove } from '../util/arrRemove';\nexport class AsyncAction extends Action {\n constructor(scheduler, work) {\n super(scheduler, work);\n this.scheduler = scheduler;\n this.work = work;\n this.pending = false;\n }\n schedule(state, delay = 0) {\n if (this.closed) {\n return this;\n }\n this.state = state;\n const id = this.id;\n const scheduler = this.scheduler;\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, delay);\n }\n this.pending = true;\n this.delay = delay;\n this.id = this.id || this.requestAsyncId(scheduler, this.id, delay);\n return this;\n }\n requestAsyncId(scheduler, _id, delay = 0) {\n return intervalProvider.setInterval(scheduler.flush.bind(scheduler, this), delay);\n }\n recycleAsyncId(_scheduler, id, delay = 0) {\n if (delay != null && this.delay === delay && this.pending === false) {\n return id;\n }\n intervalProvider.clearInterval(id);\n return undefined;\n }\n execute(state, delay) {\n if (this.closed) {\n return new Error('executing a cancelled action');\n }\n this.pending = false;\n const error = this._execute(state, delay);\n if (error) {\n return error;\n }\n else if (this.pending === false && this.id != null) {\n this.id = this.recycleAsyncId(this.scheduler, this.id, null);\n }\n }\n _execute(state, _delay) {\n let errored = false;\n let errorValue = undefined;\n try {\n this.work(state);\n }\n catch (e) {\n errored = true;\n errorValue = (!!e && e) || new Error(e);\n }\n if (errored) {\n this.unsubscribe();\n return errorValue;\n }\n }\n unsubscribe() {\n if (!this.closed) {\n const { id, scheduler } = this;\n const { actions } = scheduler;\n this.work = this.state = this.scheduler = null;\n this.pending = false;\n arrRemove(actions, this);\n if (id != null) {\n this.id = this.recycleAsyncId(scheduler, id, null);\n }\n this.delay = null;\n super.unsubscribe();\n }\n }\n}\n//# sourceMappingURL=AsyncAction.js.map","import { dateTimestampProvider } from \"./scheduler/dateTimestampProvider\";\nexport class Scheduler {\n constructor(SchedulerAction, now = Scheduler.now) {\n this.SchedulerAction = SchedulerAction;\n this.now = now;\n }\n schedule(work, delay = 0, state) {\n return new this.SchedulerAction(this, work).schedule(state, delay);\n }\n}\nScheduler.now = dateTimestampProvider.now;\n//# sourceMappingURL=Scheduler.js.map","import { Scheduler } from '../Scheduler';\nexport class AsyncScheduler extends Scheduler {\n constructor(SchedulerAction, now = Scheduler.now) {\n super(SchedulerAction, now);\n this.actions = [];\n this.active = false;\n this.scheduled = undefined;\n }\n flush(action) {\n const { actions } = this;\n if (this.active) {\n actions.push(action);\n return;\n }\n let error;\n this.active = true;\n do {\n if (error = action.execute(action.state, action.delay)) {\n break;\n }\n } while (action = actions.shift());\n this.active = false;\n if (error) {\n while (action = actions.shift()) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n//# sourceMappingURL=AsyncScheduler.js.map","/*!\n * clipboard.js v2.0.6\n * https://clipboardjs.com/\n * \n * Licensed MIT © Zeno Rocha\n */\n(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"ClipboardJS\"] = factory();\n\telse\n\t\troot[\"ClipboardJS\"] = factory();\n})(this, function() {\nreturn /******/ (function(modules) { // webpackBootstrap\n/******/ \t// The module cache\n/******/ \tvar installedModules = {};\n/******/\n/******/ \t// The require function\n/******/ \tfunction __webpack_require__(moduleId) {\n/******/\n/******/ \t\t// Check if module is in cache\n/******/ \t\tif(installedModules[moduleId]) {\n/******/ \t\t\treturn installedModules[moduleId].exports;\n/******/ \t\t}\n/******/ \t\t// Create a new module (and put it into the cache)\n/******/ \t\tvar module = installedModules[moduleId] = {\n/******/ \t\t\ti: moduleId,\n/******/ \t\t\tl: false,\n/******/ \t\t\texports: {}\n/******/ \t\t};\n/******/\n/******/ \t\t// Execute the module function\n/******/ \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n/******/\n/******/ \t\t// Flag the module as loaded\n/******/ \t\tmodule.l = true;\n/******/\n/******/ \t\t// Return the exports of the module\n/******/ \t\treturn module.exports;\n/******/ \t}\n/******/\n/******/\n/******/ \t// expose the modules object (__webpack_modules__)\n/******/ \t__webpack_require__.m = modules;\n/******/\n/******/ \t// expose the module cache\n/******/ \t__webpack_require__.c = installedModules;\n/******/\n/******/ \t// define getter function for harmony exports\n/******/ \t__webpack_require__.d = function(exports, name, getter) {\n/******/ \t\tif(!__webpack_require__.o(exports, name)) {\n/******/ \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n/******/ \t\t}\n/******/ \t};\n/******/\n/******/ \t// define __esModule on exports\n/******/ \t__webpack_require__.r = function(exports) {\n/******/ \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n/******/ \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n/******/ \t\t}\n/******/ \t\tObject.defineProperty(exports, '__esModule', { value: true });\n/******/ \t};\n/******/\n/******/ \t// create a fake namespace object\n/******/ \t// mode & 1: value is a module id, require it\n/******/ \t// mode & 2: merge all properties of value into the ns\n/******/ \t// mode & 4: return value when already ns object\n/******/ \t// mode & 8|1: behave like require\n/******/ \t__webpack_require__.t = function(value, mode) {\n/******/ \t\tif(mode & 1) value = __webpack_require__(value);\n/******/ \t\tif(mode & 8) return value;\n/******/ \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n/******/ \t\tvar ns = Object.create(null);\n/******/ \t\t__webpack_require__.r(ns);\n/******/ \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n/******/ \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n/******/ \t\treturn ns;\n/******/ \t};\n/******/\n/******/ \t// getDefaultExport function for compatibility with non-harmony modules\n/******/ \t__webpack_require__.n = function(module) {\n/******/ \t\tvar getter = module && module.__esModule ?\n/******/ \t\t\tfunction getDefault() { return module['default']; } :\n/******/ \t\t\tfunction getModuleExports() { return module; };\n/******/ \t\t__webpack_require__.d(getter, 'a', getter);\n/******/ \t\treturn getter;\n/******/ \t};\n/******/\n/******/ \t// Object.prototype.hasOwnProperty.call\n/******/ \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n/******/\n/******/ \t// __webpack_public_path__\n/******/ \t__webpack_require__.p = \"\";\n/******/\n/******/\n/******/ \t// Load entry module and return exports\n/******/ \treturn __webpack_require__(__webpack_require__.s = 6);\n/******/ })\n/************************************************************************/\n/******/ ([\n/* 0 */\n/***/ (function(module, exports) {\n\nfunction select(element) {\n var selectedText;\n\n if (element.nodeName === 'SELECT') {\n element.focus();\n\n selectedText = element.value;\n }\n else if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {\n var isReadOnly = element.hasAttribute('readonly');\n\n if (!isReadOnly) {\n element.setAttribute('readonly', '');\n }\n\n element.select();\n element.setSelectionRange(0, element.value.length);\n\n if (!isReadOnly) {\n element.removeAttribute('readonly');\n }\n\n selectedText = element.value;\n }\n else {\n if (element.hasAttribute('contenteditable')) {\n element.focus();\n }\n\n var selection = window.getSelection();\n var range = document.createRange();\n\n range.selectNodeContents(element);\n selection.removeAllRanges();\n selection.addRange(range);\n\n selectedText = selection.toString();\n }\n\n return selectedText;\n}\n\nmodule.exports = select;\n\n\n/***/ }),\n/* 1 */\n/***/ (function(module, exports) {\n\nfunction E () {\n // Keep this empty so it's easier to inherit from\n // (via https://github.com/lipsmack from https://github.com/scottcorgan/tiny-emitter/issues/3)\n}\n\nE.prototype = {\n on: function (name, callback, ctx) {\n var e = this.e || (this.e = {});\n\n (e[name] || (e[name] = [])).push({\n fn: callback,\n ctx: ctx\n });\n\n return this;\n },\n\n once: function (name, callback, ctx) {\n var self = this;\n function listener () {\n self.off(name, listener);\n callback.apply(ctx, arguments);\n };\n\n listener._ = callback\n return this.on(name, listener, ctx);\n },\n\n emit: function (name) {\n var data = [].slice.call(arguments, 1);\n var evtArr = ((this.e || (this.e = {}))[name] || []).slice();\n var i = 0;\n var len = evtArr.length;\n\n for (i; i < len; i++) {\n evtArr[i].fn.apply(evtArr[i].ctx, data);\n }\n\n return this;\n },\n\n off: function (name, callback) {\n var e = this.e || (this.e = {});\n var evts = e[name];\n var liveEvents = [];\n\n if (evts && callback) {\n for (var i = 0, len = evts.length; i < len; i++) {\n if (evts[i].fn !== callback && evts[i].fn._ !== callback)\n liveEvents.push(evts[i]);\n }\n }\n\n // Remove event from queue to prevent memory leak\n // Suggested by https://github.com/lazd\n // Ref: https://github.com/scottcorgan/tiny-emitter/commit/c6ebfaa9bc973b33d110a84a307742b7cf94c953#commitcomment-5024910\n\n (liveEvents.length)\n ? e[name] = liveEvents\n : delete e[name];\n\n return this;\n }\n};\n\nmodule.exports = E;\nmodule.exports.TinyEmitter = E;\n\n\n/***/ }),\n/* 2 */\n/***/ (function(module, exports, __webpack_require__) {\n\nvar is = __webpack_require__(3);\nvar delegate = __webpack_require__(4);\n\n/**\n * Validates all params and calls the right\n * listener function based on its target type.\n *\n * @param {String|HTMLElement|HTMLCollection|NodeList} target\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listen(target, type, callback) {\n if (!target && !type && !callback) {\n throw new Error('Missing required arguments');\n }\n\n if (!is.string(type)) {\n throw new TypeError('Second argument must be a String');\n }\n\n if (!is.fn(callback)) {\n throw new TypeError('Third argument must be a Function');\n }\n\n if (is.node(target)) {\n return listenNode(target, type, callback);\n }\n else if (is.nodeList(target)) {\n return listenNodeList(target, type, callback);\n }\n else if (is.string(target)) {\n return listenSelector(target, type, callback);\n }\n else {\n throw new TypeError('First argument must be a String, HTMLElement, HTMLCollection, or NodeList');\n }\n}\n\n/**\n * Adds an event listener to a HTML element\n * and returns a remove listener function.\n *\n * @param {HTMLElement} node\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNode(node, type, callback) {\n node.addEventListener(type, callback);\n\n return {\n destroy: function() {\n node.removeEventListener(type, callback);\n }\n }\n}\n\n/**\n * Add an event listener to a list of HTML elements\n * and returns a remove listener function.\n *\n * @param {NodeList|HTMLCollection} nodeList\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenNodeList(nodeList, type, callback) {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.addEventListener(type, callback);\n });\n\n return {\n destroy: function() {\n Array.prototype.forEach.call(nodeList, function(node) {\n node.removeEventListener(type, callback);\n });\n }\n }\n}\n\n/**\n * Add an event listener to a selector\n * and returns a remove listener function.\n *\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Object}\n */\nfunction listenSelector(selector, type, callback) {\n return delegate(document.body, selector, type, callback);\n}\n\nmodule.exports = listen;\n\n\n/***/ }),\n/* 3 */\n/***/ (function(module, exports) {\n\n/**\n * Check if argument is a HTML element.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.node = function(value) {\n return value !== undefined\n && value instanceof HTMLElement\n && value.nodeType === 1;\n};\n\n/**\n * Check if argument is a list of HTML elements.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.nodeList = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return value !== undefined\n && (type === '[object NodeList]' || type === '[object HTMLCollection]')\n && ('length' in value)\n && (value.length === 0 || exports.node(value[0]));\n};\n\n/**\n * Check if argument is a string.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.string = function(value) {\n return typeof value === 'string'\n || value instanceof String;\n};\n\n/**\n * Check if argument is a function.\n *\n * @param {Object} value\n * @return {Boolean}\n */\nexports.fn = function(value) {\n var type = Object.prototype.toString.call(value);\n\n return type === '[object Function]';\n};\n\n\n/***/ }),\n/* 4 */\n/***/ (function(module, exports, __webpack_require__) {\n\nvar closest = __webpack_require__(5);\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction _delegate(element, selector, type, callback, useCapture) {\n var listenerFn = listener.apply(this, arguments);\n\n element.addEventListener(type, listenerFn, useCapture);\n\n return {\n destroy: function() {\n element.removeEventListener(type, listenerFn, useCapture);\n }\n }\n}\n\n/**\n * Delegates event to a selector.\n *\n * @param {Element|String|Array} [elements]\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @param {Boolean} useCapture\n * @return {Object}\n */\nfunction delegate(elements, selector, type, callback, useCapture) {\n // Handle the regular Element usage\n if (typeof elements.addEventListener === 'function') {\n return _delegate.apply(null, arguments);\n }\n\n // Handle Element-less usage, it defaults to global delegation\n if (typeof type === 'function') {\n // Use `document` as the first parameter, then apply arguments\n // This is a short way to .unshift `arguments` without running into deoptimizations\n return _delegate.bind(null, document).apply(null, arguments);\n }\n\n // Handle Selector-based usage\n if (typeof elements === 'string') {\n elements = document.querySelectorAll(elements);\n }\n\n // Handle Array-like based usage\n return Array.prototype.map.call(elements, function (element) {\n return _delegate(element, selector, type, callback, useCapture);\n });\n}\n\n/**\n * Finds closest match and invokes callback.\n *\n * @param {Element} element\n * @param {String} selector\n * @param {String} type\n * @param {Function} callback\n * @return {Function}\n */\nfunction listener(element, selector, type, callback) {\n return function(e) {\n e.delegateTarget = closest(e.target, selector);\n\n if (e.delegateTarget) {\n callback.call(element, e);\n }\n }\n}\n\nmodule.exports = delegate;\n\n\n/***/ }),\n/* 5 */\n/***/ (function(module, exports) {\n\nvar DOCUMENT_NODE_TYPE = 9;\n\n/**\n * A polyfill for Element.matches()\n */\nif (typeof Element !== 'undefined' && !Element.prototype.matches) {\n var proto = Element.prototype;\n\n proto.matches = proto.matchesSelector ||\n proto.mozMatchesSelector ||\n proto.msMatchesSelector ||\n proto.oMatchesSelector ||\n proto.webkitMatchesSelector;\n}\n\n/**\n * Finds the closest parent that matches a selector.\n *\n * @param {Element} element\n * @param {String} selector\n * @return {Function}\n */\nfunction closest (element, selector) {\n while (element && element.nodeType !== DOCUMENT_NODE_TYPE) {\n if (typeof element.matches === 'function' &&\n element.matches(selector)) {\n return element;\n }\n element = element.parentNode;\n }\n}\n\nmodule.exports = closest;\n\n\n/***/ }),\n/* 6 */\n/***/ (function(module, __webpack_exports__, __webpack_require__) {\n\n\"use strict\";\n__webpack_require__.r(__webpack_exports__);\n\n// EXTERNAL MODULE: ./node_modules/select/src/select.js\nvar src_select = __webpack_require__(0);\nvar select_default = /*#__PURE__*/__webpack_require__.n(src_select);\n\n// CONCATENATED MODULE: ./src/clipboard-action.js\nvar _typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\nvar _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nfunction _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\n\n\n/**\n * Inner class which performs selection from either `text` or `target`\n * properties and then executes copy or cut operations.\n */\n\nvar clipboard_action_ClipboardAction = function () {\n /**\n * @param {Object} options\n */\n function ClipboardAction(options) {\n _classCallCheck(this, ClipboardAction);\n\n this.resolveOptions(options);\n this.initSelection();\n }\n\n /**\n * Defines base properties passed from constructor.\n * @param {Object} options\n */\n\n\n _createClass(ClipboardAction, [{\n key: 'resolveOptions',\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n\n this.action = options.action;\n this.container = options.container;\n this.emitter = options.emitter;\n this.target = options.target;\n this.text = options.text;\n this.trigger = options.trigger;\n\n this.selectedText = '';\n }\n\n /**\n * Decides which selection strategy is going to be applied based\n * on the existence of `text` and `target` properties.\n */\n\n }, {\n key: 'initSelection',\n value: function initSelection() {\n if (this.text) {\n this.selectFake();\n } else if (this.target) {\n this.selectTarget();\n }\n }\n\n /**\n * Creates a fake textarea element, sets its value from `text` property,\n * and makes a selection on it.\n */\n\n }, {\n key: 'selectFake',\n value: function selectFake() {\n var _this = this;\n\n var isRTL = document.documentElement.getAttribute('dir') == 'rtl';\n\n this.removeFake();\n\n this.fakeHandlerCallback = function () {\n return _this.removeFake();\n };\n this.fakeHandler = this.container.addEventListener('click', this.fakeHandlerCallback) || true;\n\n this.fakeElem = document.createElement('textarea');\n // Prevent zooming on iOS\n this.fakeElem.style.fontSize = '12pt';\n // Reset box model\n this.fakeElem.style.border = '0';\n this.fakeElem.style.padding = '0';\n this.fakeElem.style.margin = '0';\n // Move element out of screen horizontally\n this.fakeElem.style.position = 'absolute';\n this.fakeElem.style[isRTL ? 'right' : 'left'] = '-9999px';\n // Move element to the same position vertically\n var yPosition = window.pageYOffset || document.documentElement.scrollTop;\n this.fakeElem.style.top = yPosition + 'px';\n\n this.fakeElem.setAttribute('readonly', '');\n this.fakeElem.value = this.text;\n\n this.container.appendChild(this.fakeElem);\n\n this.selectedText = select_default()(this.fakeElem);\n this.copyText();\n }\n\n /**\n * Only removes the fake element after another click event, that way\n * a user can hit `Ctrl+C` to copy because selection still exists.\n */\n\n }, {\n key: 'removeFake',\n value: function removeFake() {\n if (this.fakeHandler) {\n this.container.removeEventListener('click', this.fakeHandlerCallback);\n this.fakeHandler = null;\n this.fakeHandlerCallback = null;\n }\n\n if (this.fakeElem) {\n this.container.removeChild(this.fakeElem);\n this.fakeElem = null;\n }\n }\n\n /**\n * Selects the content from element passed on `target` property.\n */\n\n }, {\n key: 'selectTarget',\n value: function selectTarget() {\n this.selectedText = select_default()(this.target);\n this.copyText();\n }\n\n /**\n * Executes the copy operation based on the current selection.\n */\n\n }, {\n key: 'copyText',\n value: function copyText() {\n var succeeded = void 0;\n\n try {\n succeeded = document.execCommand(this.action);\n } catch (err) {\n succeeded = false;\n }\n\n this.handleResult(succeeded);\n }\n\n /**\n * Fires an event based on the copy operation result.\n * @param {Boolean} succeeded\n */\n\n }, {\n key: 'handleResult',\n value: function handleResult(succeeded) {\n this.emitter.emit(succeeded ? 'success' : 'error', {\n action: this.action,\n text: this.selectedText,\n trigger: this.trigger,\n clearSelection: this.clearSelection.bind(this)\n });\n }\n\n /**\n * Moves focus away from `target` and back to the trigger, removes current selection.\n */\n\n }, {\n key: 'clearSelection',\n value: function clearSelection() {\n if (this.trigger) {\n this.trigger.focus();\n }\n document.activeElement.blur();\n window.getSelection().removeAllRanges();\n }\n\n /**\n * Sets the `action` to be performed which can be either 'copy' or 'cut'.\n * @param {String} action\n */\n\n }, {\n key: 'destroy',\n\n\n /**\n * Destroy lifecycle.\n */\n value: function destroy() {\n this.removeFake();\n }\n }, {\n key: 'action',\n set: function set() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'copy';\n\n this._action = action;\n\n if (this._action !== 'copy' && this._action !== 'cut') {\n throw new Error('Invalid \"action\" value, use either \"copy\" or \"cut\"');\n }\n }\n\n /**\n * Gets the `action` property.\n * @return {String}\n */\n ,\n get: function get() {\n return this._action;\n }\n\n /**\n * Sets the `target` property using an element\n * that will be have its content copied.\n * @param {Element} target\n */\n\n }, {\n key: 'target',\n set: function set(target) {\n if (target !== undefined) {\n if (target && (typeof target === 'undefined' ? 'undefined' : _typeof(target)) === 'object' && target.nodeType === 1) {\n if (this.action === 'copy' && target.hasAttribute('disabled')) {\n throw new Error('Invalid \"target\" attribute. Please use \"readonly\" instead of \"disabled\" attribute');\n }\n\n if (this.action === 'cut' && (target.hasAttribute('readonly') || target.hasAttribute('disabled'))) {\n throw new Error('Invalid \"target\" attribute. You can\\'t cut text from elements with \"readonly\" or \"disabled\" attributes');\n }\n\n this._target = target;\n } else {\n throw new Error('Invalid \"target\" value, use a valid Element');\n }\n }\n }\n\n /**\n * Gets the `target` property.\n * @return {String|HTMLElement}\n */\n ,\n get: function get() {\n return this._target;\n }\n }]);\n\n return ClipboardAction;\n}();\n\n/* harmony default export */ var clipboard_action = (clipboard_action_ClipboardAction);\n// EXTERNAL MODULE: ./node_modules/tiny-emitter/index.js\nvar tiny_emitter = __webpack_require__(1);\nvar tiny_emitter_default = /*#__PURE__*/__webpack_require__.n(tiny_emitter);\n\n// EXTERNAL MODULE: ./node_modules/good-listener/src/listen.js\nvar listen = __webpack_require__(2);\nvar listen_default = /*#__PURE__*/__webpack_require__.n(listen);\n\n// CONCATENATED MODULE: ./src/clipboard.js\nvar clipboard_typeof = typeof Symbol === \"function\" && typeof Symbol.iterator === \"symbol\" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === \"function\" && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj; };\n\nvar clipboard_createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if (\"value\" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();\n\nfunction clipboard_classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(\"Cannot call a class as a function\"); } }\n\nfunction _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError(\"this hasn't been initialised - super() hasn't been called\"); } return call && (typeof call === \"object\" || typeof call === \"function\") ? call : self; }\n\nfunction _inherits(subClass, superClass) { if (typeof superClass !== \"function\" && superClass !== null) { throw new TypeError(\"Super expression must either be null or a function, not \" + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }\n\n\n\n\n\n/**\n * Base class which takes one or more elements, adds event listeners to them,\n * and instantiates a new `ClipboardAction` on each click.\n */\n\nvar clipboard_Clipboard = function (_Emitter) {\n _inherits(Clipboard, _Emitter);\n\n /**\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n * @param {Object} options\n */\n function Clipboard(trigger, options) {\n clipboard_classCallCheck(this, Clipboard);\n\n var _this = _possibleConstructorReturn(this, (Clipboard.__proto__ || Object.getPrototypeOf(Clipboard)).call(this));\n\n _this.resolveOptions(options);\n _this.listenClick(trigger);\n return _this;\n }\n\n /**\n * Defines if attributes would be resolved using internal setter functions\n * or custom functions that were passed in the constructor.\n * @param {Object} options\n */\n\n\n clipboard_createClass(Clipboard, [{\n key: 'resolveOptions',\n value: function resolveOptions() {\n var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};\n\n this.action = typeof options.action === 'function' ? options.action : this.defaultAction;\n this.target = typeof options.target === 'function' ? options.target : this.defaultTarget;\n this.text = typeof options.text === 'function' ? options.text : this.defaultText;\n this.container = clipboard_typeof(options.container) === 'object' ? options.container : document.body;\n }\n\n /**\n * Adds a click event listener to the passed trigger.\n * @param {String|HTMLElement|HTMLCollection|NodeList} trigger\n */\n\n }, {\n key: 'listenClick',\n value: function listenClick(trigger) {\n var _this2 = this;\n\n this.listener = listen_default()(trigger, 'click', function (e) {\n return _this2.onClick(e);\n });\n }\n\n /**\n * Defines a new `ClipboardAction` on each click event.\n * @param {Event} e\n */\n\n }, {\n key: 'onClick',\n value: function onClick(e) {\n var trigger = e.delegateTarget || e.currentTarget;\n\n if (this.clipboardAction) {\n this.clipboardAction = null;\n }\n\n this.clipboardAction = new clipboard_action({\n action: this.action(trigger),\n target: this.target(trigger),\n text: this.text(trigger),\n container: this.container,\n trigger: trigger,\n emitter: this\n });\n }\n\n /**\n * Default `action` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: 'defaultAction',\n value: function defaultAction(trigger) {\n return getAttributeValue('action', trigger);\n }\n\n /**\n * Default `target` lookup function.\n * @param {Element} trigger\n */\n\n }, {\n key: 'defaultTarget',\n value: function defaultTarget(trigger) {\n var selector = getAttributeValue('target', trigger);\n\n if (selector) {\n return document.querySelector(selector);\n }\n }\n\n /**\n * Returns the support of the given action, or all actions if no action is\n * given.\n * @param {String} [action]\n */\n\n }, {\n key: 'defaultText',\n\n\n /**\n * Default `text` lookup function.\n * @param {Element} trigger\n */\n value: function defaultText(trigger) {\n return getAttributeValue('text', trigger);\n }\n\n /**\n * Destroy lifecycle.\n */\n\n }, {\n key: 'destroy',\n value: function destroy() {\n this.listener.destroy();\n\n if (this.clipboardAction) {\n this.clipboardAction.destroy();\n this.clipboardAction = null;\n }\n }\n }], [{\n key: 'isSupported',\n value: function isSupported() {\n var action = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : ['copy', 'cut'];\n\n var actions = typeof action === 'string' ? [action] : action;\n var support = !!document.queryCommandSupported;\n\n actions.forEach(function (action) {\n support = support && !!document.queryCommandSupported(action);\n });\n\n return support;\n }\n }]);\n\n return Clipboard;\n}(tiny_emitter_default.a);\n\n/**\n * Helper function to retrieve attribute value.\n * @param {String} suffix\n * @param {Element} element\n */\n\n\nfunction getAttributeValue(suffix, element) {\n var attribute = 'data-clipboard-' + suffix;\n\n if (!element.hasAttribute(attribute)) {\n return;\n }\n\n return element.getAttribute(attribute);\n}\n\n/* harmony default export */ var clipboard = __webpack_exports__[\"default\"] = (clipboard_Clipboard);\n\n/***/ })\n/******/ ])[\"default\"];\n});","import { isScheduler } from '../util/isScheduler';\nimport { fromArray } from './fromArray';\nimport { scheduleArray } from '../scheduled/scheduleArray';\nexport function of(...args) {\n let scheduler = args[args.length - 1];\n if (isScheduler(scheduler)) {\n args.pop();\n return scheduleArray(args, scheduler);\n }\n else {\n return fromArray(args);\n }\n}\n//# sourceMappingURL=of.js.map","const { isArray } = Array;\nconst { getPrototypeOf, prototype: objectProto, keys: getKeys } = Object;\nexport function argsArgArrayOrObject(args) {\n if (args.length === 1) {\n const first = args[0];\n if (isArray(first)) {\n return { args: first, keys: null };\n }\n if (isPOJO(first)) {\n const keys = getKeys(first);\n return {\n args: keys.map((key) => first[key]),\n keys,\n };\n }\n }\n return { args: args, keys: null };\n}\nfunction isPOJO(obj) {\n return obj && typeof obj === 'object' && getPrototypeOf(obj) === objectProto;\n}\n//# sourceMappingURL=argsArgArrayOrObject.js.map","import { Observable } from '../Observable';\nimport { isScheduler } from '../util/isScheduler';\nimport { argsArgArrayOrObject } from '../util/argsArgArrayOrObject';\nimport { Subscriber } from '../Subscriber';\nimport { from } from './from';\nimport { identity } from '../util/identity';\nimport { mapOneOrManyArgs } from '../util/mapOneOrManyArgs';\nexport function combineLatest(...args) {\n let resultSelector = undefined;\n let scheduler = undefined;\n if (isScheduler(args[args.length - 1])) {\n scheduler = args.pop();\n }\n if (typeof args[args.length - 1] === 'function') {\n resultSelector = args.pop();\n }\n const { args: observables, keys } = argsArgArrayOrObject(args);\n const result = new Observable(combineLatestInit(observables, scheduler, keys\n ?\n (args) => {\n const value = {};\n for (let i = 0; i < args.length; i++) {\n value[keys[i]] = args[i];\n }\n return value;\n }\n :\n identity));\n if (resultSelector) {\n return result.pipe(mapOneOrManyArgs(resultSelector));\n }\n return result;\n}\nclass CombineLatestSubscriber extends Subscriber {\n constructor(destination, _next, shouldComplete) {\n super(destination);\n this._next = _next;\n this.shouldComplete = shouldComplete;\n }\n _complete() {\n if (this.shouldComplete()) {\n super._complete();\n }\n else {\n this.unsubscribe();\n }\n }\n}\nexport function combineLatestInit(observables, scheduler = undefined, valueTransform = identity) {\n return (subscriber) => {\n const primarySubscribe = () => {\n const { length } = observables;\n const values = new Array(length);\n let active = length;\n const hasValues = observables.map(() => false);\n let waitingForFirstValues = true;\n const emit = () => subscriber.next(valueTransform(values.slice()));\n for (let i = 0; i < length; i++) {\n const subscribe = () => {\n const source = from(observables[i], scheduler);\n source.subscribe(new CombineLatestSubscriber(subscriber, (value) => {\n values[i] = value;\n if (waitingForFirstValues) {\n hasValues[i] = true;\n waitingForFirstValues = !hasValues.every(identity);\n }\n if (!waitingForFirstValues) {\n emit();\n }\n }, () => --active === 0));\n };\n maybeSchedule(scheduler, subscribe, subscriber);\n }\n };\n maybeSchedule(scheduler, primarySubscribe, subscriber);\n };\n}\nfunction maybeSchedule(scheduler, execute, subscription) {\n if (scheduler) {\n subscription.add(scheduler.schedule(execute));\n }\n else {\n execute();\n }\n}\n//# sourceMappingURL=combineLatest.js.map","import { Subject } from './Subject';\nimport { dateTimestampProvider } from './scheduler/dateTimestampProvider';\nexport class ReplaySubject extends Subject {\n constructor(bufferSize = Infinity, windowTime = Infinity, timestampProvider = dateTimestampProvider) {\n super();\n this.bufferSize = bufferSize;\n this.windowTime = windowTime;\n this.timestampProvider = timestampProvider;\n this.buffer = [];\n this.infiniteTimeWindow = true;\n this.infiniteTimeWindow = windowTime === Infinity;\n this.bufferSize = Math.max(1, bufferSize);\n this.windowTime = Math.max(1, windowTime);\n }\n next(value) {\n const { isStopped, buffer, infiniteTimeWindow, timestampProvider, windowTime } = this;\n if (!isStopped) {\n buffer.push(value);\n !infiniteTimeWindow && buffer.push(timestampProvider.now() + windowTime);\n }\n this.trimBuffer();\n super.next(value);\n }\n _subscribe(subscriber) {\n this._throwIfClosed();\n this.trimBuffer();\n const subscription = this._innerSubscribe(subscriber);\n const { infiniteTimeWindow, buffer } = this;\n const copy = buffer.slice();\n for (let i = 0; i < copy.length && !subscriber.closed; i += infiniteTimeWindow ? 1 : 2) {\n subscriber.next(copy[i]);\n }\n this._checkFinalizedStatuses(subscriber);\n return subscription;\n }\n trimBuffer() {\n const { bufferSize, timestampProvider, buffer, infiniteTimeWindow } = this;\n const adjustedBufferSize = (infiniteTimeWindow ? 1 : 2) * bufferSize;\n bufferSize < Infinity && adjustedBufferSize < buffer.length && buffer.splice(0, buffer.length - adjustedBufferSize);\n if (!infiniteTimeWindow) {\n const now = timestampProvider.now();\n let last = 0;\n for (let i = 1; i < buffer.length && buffer[i] <= now; i += 2) {\n last = i;\n }\n last && buffer.splice(0, last + 1);\n }\n }\n}\n//# sourceMappingURL=ReplaySubject.js.map","/**\r\n * A collection of shims that provide minimal functionality of the ES6 collections.\r\n *\r\n * These implementations are not meant to be used outside of the ResizeObserver\r\n * modules as they cover only a limited range of use cases.\r\n */\r\n/* eslint-disable require-jsdoc, valid-jsdoc */\r\nvar MapShim = (function () {\r\n if (typeof Map !== 'undefined') {\r\n return Map;\r\n }\r\n /**\r\n * Returns index in provided array that matches the specified key.\r\n *\r\n * @param {Array} arr\r\n * @param {*} key\r\n * @returns {number}\r\n */\r\n function getIndex(arr, key) {\r\n var result = -1;\r\n arr.some(function (entry, index) {\r\n if (entry[0] === key) {\r\n result = index;\r\n return true;\r\n }\r\n return false;\r\n });\r\n return result;\r\n }\r\n return /** @class */ (function () {\r\n function class_1() {\r\n this.__entries__ = [];\r\n }\r\n Object.defineProperty(class_1.prototype, \"size\", {\r\n /**\r\n * @returns {boolean}\r\n */\r\n get: function () {\r\n return this.__entries__.length;\r\n },\r\n enumerable: true,\r\n configurable: true\r\n });\r\n /**\r\n * @param {*} key\r\n * @returns {*}\r\n */\r\n class_1.prototype.get = function (key) {\r\n var index = getIndex(this.__entries__, key);\r\n var entry = this.__entries__[index];\r\n return entry && entry[1];\r\n };\r\n /**\r\n * @param {*} key\r\n * @param {*} value\r\n * @returns {void}\r\n */\r\n class_1.prototype.set = function (key, value) {\r\n var index = getIndex(this.__entries__, key);\r\n if (~index) {\r\n this.__entries__[index][1] = value;\r\n }\r\n else {\r\n this.__entries__.push([key, value]);\r\n }\r\n };\r\n /**\r\n * @param {*} key\r\n * @returns {void}\r\n */\r\n class_1.prototype.delete = function (key) {\r\n var entries = this.__entries__;\r\n var index = getIndex(entries, key);\r\n if (~index) {\r\n entries.splice(index, 1);\r\n }\r\n };\r\n /**\r\n * @param {*} key\r\n * @returns {void}\r\n */\r\n class_1.prototype.has = function (key) {\r\n return !!~getIndex(this.__entries__, key);\r\n };\r\n /**\r\n * @returns {void}\r\n */\r\n class_1.prototype.clear = function () {\r\n this.__entries__.splice(0);\r\n };\r\n /**\r\n * @param {Function} callback\r\n * @param {*} [ctx=null]\r\n * @returns {void}\r\n */\r\n class_1.prototype.forEach = function (callback, ctx) {\r\n if (ctx === void 0) { ctx = null; }\r\n for (var _i = 0, _a = this.__entries__; _i < _a.length; _i++) {\r\n var entry = _a[_i];\r\n callback.call(ctx, entry[1], entry[0]);\r\n }\r\n };\r\n return class_1;\r\n }());\r\n})();\n\n/**\r\n * Detects whether window and document objects are available in current environment.\r\n */\r\nvar isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && window.document === document;\n\n// Returns global object of a current environment.\r\nvar global$1 = (function () {\r\n if (typeof global !== 'undefined' && global.Math === Math) {\r\n return global;\r\n }\r\n if (typeof self !== 'undefined' && self.Math === Math) {\r\n return self;\r\n }\r\n if (typeof window !== 'undefined' && window.Math === Math) {\r\n return window;\r\n }\r\n // eslint-disable-next-line no-new-func\r\n return Function('return this')();\r\n})();\n\n/**\r\n * A shim for the requestAnimationFrame which falls back to the setTimeout if\r\n * first one is not supported.\r\n *\r\n * @returns {number} Requests' identifier.\r\n */\r\nvar requestAnimationFrame$1 = (function () {\r\n if (typeof requestAnimationFrame === 'function') {\r\n // It's required to use a bounded function because IE sometimes throws\r\n // an \"Invalid calling object\" error if rAF is invoked without the global\r\n // object on the left hand side.\r\n return requestAnimationFrame.bind(global$1);\r\n }\r\n return function (callback) { return setTimeout(function () { return callback(Date.now()); }, 1000 / 60); };\r\n})();\n\n// Defines minimum timeout before adding a trailing call.\r\nvar trailingTimeout = 2;\r\n/**\r\n * Creates a wrapper function which ensures that provided callback will be\r\n * invoked only once during the specified delay period.\r\n *\r\n * @param {Function} callback - Function to be invoked after the delay period.\r\n * @param {number} delay - Delay after which to invoke callback.\r\n * @returns {Function}\r\n */\r\nfunction throttle (callback, delay) {\r\n var leadingCall = false, trailingCall = false, lastCallTime = 0;\r\n /**\r\n * Invokes the original callback function and schedules new invocation if\r\n * the \"proxy\" was called during current request.\r\n *\r\n * @returns {void}\r\n */\r\n function resolvePending() {\r\n if (leadingCall) {\r\n leadingCall = false;\r\n callback();\r\n }\r\n if (trailingCall) {\r\n proxy();\r\n }\r\n }\r\n /**\r\n * Callback invoked after the specified delay. It will further postpone\r\n * invocation of the original function delegating it to the\r\n * requestAnimationFrame.\r\n *\r\n * @returns {void}\r\n */\r\n function timeoutCallback() {\r\n requestAnimationFrame$1(resolvePending);\r\n }\r\n /**\r\n * Schedules invocation of the original function.\r\n *\r\n * @returns {void}\r\n */\r\n function proxy() {\r\n var timeStamp = Date.now();\r\n if (leadingCall) {\r\n // Reject immediately following calls.\r\n if (timeStamp - lastCallTime < trailingTimeout) {\r\n return;\r\n }\r\n // Schedule new call to be in invoked when the pending one is resolved.\r\n // This is important for \"transitions\" which never actually start\r\n // immediately so there is a chance that we might miss one if change\r\n // happens amids the pending invocation.\r\n trailingCall = true;\r\n }\r\n else {\r\n leadingCall = true;\r\n trailingCall = false;\r\n setTimeout(timeoutCallback, delay);\r\n }\r\n lastCallTime = timeStamp;\r\n }\r\n return proxy;\r\n}\n\n// Minimum delay before invoking the update of observers.\r\nvar REFRESH_DELAY = 20;\r\n// A list of substrings of CSS properties used to find transition events that\r\n// might affect dimensions of observed elements.\r\nvar transitionKeys = ['top', 'right', 'bottom', 'left', 'width', 'height', 'size', 'weight'];\r\n// Check if MutationObserver is available.\r\nvar mutationObserverSupported = typeof MutationObserver !== 'undefined';\r\n/**\r\n * Singleton controller class which handles updates of ResizeObserver instances.\r\n */\r\nvar ResizeObserverController = /** @class */ (function () {\r\n /**\r\n * Creates a new instance of ResizeObserverController.\r\n *\r\n * @private\r\n */\r\n function ResizeObserverController() {\r\n /**\r\n * Indicates whether DOM listeners have been added.\r\n *\r\n * @private {boolean}\r\n */\r\n this.connected_ = false;\r\n /**\r\n * Tells that controller has subscribed for Mutation Events.\r\n *\r\n * @private {boolean}\r\n */\r\n this.mutationEventsAdded_ = false;\r\n /**\r\n * Keeps reference to the instance of MutationObserver.\r\n *\r\n * @private {MutationObserver}\r\n */\r\n this.mutationsObserver_ = null;\r\n /**\r\n * A list of connected observers.\r\n *\r\n * @private {Array}\r\n */\r\n this.observers_ = [];\r\n this.onTransitionEnd_ = this.onTransitionEnd_.bind(this);\r\n this.refresh = throttle(this.refresh.bind(this), REFRESH_DELAY);\r\n }\r\n /**\r\n * Adds observer to observers list.\r\n *\r\n * @param {ResizeObserverSPI} observer - Observer to be added.\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.addObserver = function (observer) {\r\n if (!~this.observers_.indexOf(observer)) {\r\n this.observers_.push(observer);\r\n }\r\n // Add listeners if they haven't been added yet.\r\n if (!this.connected_) {\r\n this.connect_();\r\n }\r\n };\r\n /**\r\n * Removes observer from observers list.\r\n *\r\n * @param {ResizeObserverSPI} observer - Observer to be removed.\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.removeObserver = function (observer) {\r\n var observers = this.observers_;\r\n var index = observers.indexOf(observer);\r\n // Remove observer if it's present in registry.\r\n if (~index) {\r\n observers.splice(index, 1);\r\n }\r\n // Remove listeners if controller has no connected observers.\r\n if (!observers.length && this.connected_) {\r\n this.disconnect_();\r\n }\r\n };\r\n /**\r\n * Invokes the update of observers. It will continue running updates insofar\r\n * it detects changes.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.refresh = function () {\r\n var changesDetected = this.updateObservers_();\r\n // Continue running updates if changes have been detected as there might\r\n // be future ones caused by CSS transitions.\r\n if (changesDetected) {\r\n this.refresh();\r\n }\r\n };\r\n /**\r\n * Updates every observer from observers list and notifies them of queued\r\n * entries.\r\n *\r\n * @private\r\n * @returns {boolean} Returns \"true\" if any observer has detected changes in\r\n * dimensions of it's elements.\r\n */\r\n ResizeObserverController.prototype.updateObservers_ = function () {\r\n // Collect observers that have active observations.\r\n var activeObservers = this.observers_.filter(function (observer) {\r\n return observer.gatherActive(), observer.hasActive();\r\n });\r\n // Deliver notifications in a separate cycle in order to avoid any\r\n // collisions between observers, e.g. when multiple instances of\r\n // ResizeObserver are tracking the same element and the callback of one\r\n // of them changes content dimensions of the observed target. Sometimes\r\n // this may result in notifications being blocked for the rest of observers.\r\n activeObservers.forEach(function (observer) { return observer.broadcastActive(); });\r\n return activeObservers.length > 0;\r\n };\r\n /**\r\n * Initializes DOM listeners.\r\n *\r\n * @private\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.connect_ = function () {\r\n // Do nothing if running in a non-browser environment or if listeners\r\n // have been already added.\r\n if (!isBrowser || this.connected_) {\r\n return;\r\n }\r\n // Subscription to the \"Transitionend\" event is used as a workaround for\r\n // delayed transitions. This way it's possible to capture at least the\r\n // final state of an element.\r\n document.addEventListener('transitionend', this.onTransitionEnd_);\r\n window.addEventListener('resize', this.refresh);\r\n if (mutationObserverSupported) {\r\n this.mutationsObserver_ = new MutationObserver(this.refresh);\r\n this.mutationsObserver_.observe(document, {\r\n attributes: true,\r\n childList: true,\r\n characterData: true,\r\n subtree: true\r\n });\r\n }\r\n else {\r\n document.addEventListener('DOMSubtreeModified', this.refresh);\r\n this.mutationEventsAdded_ = true;\r\n }\r\n this.connected_ = true;\r\n };\r\n /**\r\n * Removes DOM listeners.\r\n *\r\n * @private\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.disconnect_ = function () {\r\n // Do nothing if running in a non-browser environment or if listeners\r\n // have been already removed.\r\n if (!isBrowser || !this.connected_) {\r\n return;\r\n }\r\n document.removeEventListener('transitionend', this.onTransitionEnd_);\r\n window.removeEventListener('resize', this.refresh);\r\n if (this.mutationsObserver_) {\r\n this.mutationsObserver_.disconnect();\r\n }\r\n if (this.mutationEventsAdded_) {\r\n document.removeEventListener('DOMSubtreeModified', this.refresh);\r\n }\r\n this.mutationsObserver_ = null;\r\n this.mutationEventsAdded_ = false;\r\n this.connected_ = false;\r\n };\r\n /**\r\n * \"Transitionend\" event handler.\r\n *\r\n * @private\r\n * @param {TransitionEvent} event\r\n * @returns {void}\r\n */\r\n ResizeObserverController.prototype.onTransitionEnd_ = function (_a) {\r\n var _b = _a.propertyName, propertyName = _b === void 0 ? '' : _b;\r\n // Detect whether transition may affect dimensions of an element.\r\n var isReflowProperty = transitionKeys.some(function (key) {\r\n return !!~propertyName.indexOf(key);\r\n });\r\n if (isReflowProperty) {\r\n this.refresh();\r\n }\r\n };\r\n /**\r\n * Returns instance of the ResizeObserverController.\r\n *\r\n * @returns {ResizeObserverController}\r\n */\r\n ResizeObserverController.getInstance = function () {\r\n if (!this.instance_) {\r\n this.instance_ = new ResizeObserverController();\r\n }\r\n return this.instance_;\r\n };\r\n /**\r\n * Holds reference to the controller's instance.\r\n *\r\n * @private {ResizeObserverController}\r\n */\r\n ResizeObserverController.instance_ = null;\r\n return ResizeObserverController;\r\n}());\n\n/**\r\n * Defines non-writable/enumerable properties of the provided target object.\r\n *\r\n * @param {Object} target - Object for which to define properties.\r\n * @param {Object} props - Properties to be defined.\r\n * @returns {Object} Target object.\r\n */\r\nvar defineConfigurable = (function (target, props) {\r\n for (var _i = 0, _a = Object.keys(props); _i < _a.length; _i++) {\r\n var key = _a[_i];\r\n Object.defineProperty(target, key, {\r\n value: props[key],\r\n enumerable: false,\r\n writable: false,\r\n configurable: true\r\n });\r\n }\r\n return target;\r\n});\n\n/**\r\n * Returns the global object associated with provided element.\r\n *\r\n * @param {Object} target\r\n * @returns {Object}\r\n */\r\nvar getWindowOf = (function (target) {\r\n // Assume that the element is an instance of Node, which means that it\r\n // has the \"ownerDocument\" property from which we can retrieve a\r\n // corresponding global object.\r\n var ownerGlobal = target && target.ownerDocument && target.ownerDocument.defaultView;\r\n // Return the local global object if it's not possible extract one from\r\n // provided element.\r\n return ownerGlobal || global$1;\r\n});\n\n// Placeholder of an empty content rectangle.\r\nvar emptyRect = createRectInit(0, 0, 0, 0);\r\n/**\r\n * Converts provided string to a number.\r\n *\r\n * @param {number|string} value\r\n * @returns {number}\r\n */\r\nfunction toFloat(value) {\r\n return parseFloat(value) || 0;\r\n}\r\n/**\r\n * Extracts borders size from provided styles.\r\n *\r\n * @param {CSSStyleDeclaration} styles\r\n * @param {...string} positions - Borders positions (top, right, ...)\r\n * @returns {number}\r\n */\r\nfunction getBordersSize(styles) {\r\n var positions = [];\r\n for (var _i = 1; _i < arguments.length; _i++) {\r\n positions[_i - 1] = arguments[_i];\r\n }\r\n return positions.reduce(function (size, position) {\r\n var value = styles['border-' + position + '-width'];\r\n return size + toFloat(value);\r\n }, 0);\r\n}\r\n/**\r\n * Extracts paddings sizes from provided styles.\r\n *\r\n * @param {CSSStyleDeclaration} styles\r\n * @returns {Object} Paddings box.\r\n */\r\nfunction getPaddings(styles) {\r\n var positions = ['top', 'right', 'bottom', 'left'];\r\n var paddings = {};\r\n for (var _i = 0, positions_1 = positions; _i < positions_1.length; _i++) {\r\n var position = positions_1[_i];\r\n var value = styles['padding-' + position];\r\n paddings[position] = toFloat(value);\r\n }\r\n return paddings;\r\n}\r\n/**\r\n * Calculates content rectangle of provided SVG element.\r\n *\r\n * @param {SVGGraphicsElement} target - Element content rectangle of which needs\r\n * to be calculated.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction getSVGContentRect(target) {\r\n var bbox = target.getBBox();\r\n return createRectInit(0, 0, bbox.width, bbox.height);\r\n}\r\n/**\r\n * Calculates content rectangle of provided HTMLElement.\r\n *\r\n * @param {HTMLElement} target - Element for which to calculate the content rectangle.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction getHTMLElementContentRect(target) {\r\n // Client width & height properties can't be\r\n // used exclusively as they provide rounded values.\r\n var clientWidth = target.clientWidth, clientHeight = target.clientHeight;\r\n // By this condition we can catch all non-replaced inline, hidden and\r\n // detached elements. Though elements with width & height properties less\r\n // than 0.5 will be discarded as well.\r\n //\r\n // Without it we would need to implement separate methods for each of\r\n // those cases and it's not possible to perform a precise and performance\r\n // effective test for hidden elements. E.g. even jQuery's ':visible' filter\r\n // gives wrong results for elements with width & height less than 0.5.\r\n if (!clientWidth && !clientHeight) {\r\n return emptyRect;\r\n }\r\n var styles = getWindowOf(target).getComputedStyle(target);\r\n var paddings = getPaddings(styles);\r\n var horizPad = paddings.left + paddings.right;\r\n var vertPad = paddings.top + paddings.bottom;\r\n // Computed styles of width & height are being used because they are the\r\n // only dimensions available to JS that contain non-rounded values. It could\r\n // be possible to utilize the getBoundingClientRect if only it's data wasn't\r\n // affected by CSS transformations let alone paddings, borders and scroll bars.\r\n var width = toFloat(styles.width), height = toFloat(styles.height);\r\n // Width & height include paddings and borders when the 'border-box' box\r\n // model is applied (except for IE).\r\n if (styles.boxSizing === 'border-box') {\r\n // Following conditions are required to handle Internet Explorer which\r\n // doesn't include paddings and borders to computed CSS dimensions.\r\n //\r\n // We can say that if CSS dimensions + paddings are equal to the \"client\"\r\n // properties then it's either IE, and thus we don't need to subtract\r\n // anything, or an element merely doesn't have paddings/borders styles.\r\n if (Math.round(width + horizPad) !== clientWidth) {\r\n width -= getBordersSize(styles, 'left', 'right') + horizPad;\r\n }\r\n if (Math.round(height + vertPad) !== clientHeight) {\r\n height -= getBordersSize(styles, 'top', 'bottom') + vertPad;\r\n }\r\n }\r\n // Following steps can't be applied to the document's root element as its\r\n // client[Width/Height] properties represent viewport area of the window.\r\n // Besides, it's as well not necessary as the itself neither has\r\n // rendered scroll bars nor it can be clipped.\r\n if (!isDocumentElement(target)) {\r\n // In some browsers (only in Firefox, actually) CSS width & height\r\n // include scroll bars size which can be removed at this step as scroll\r\n // bars are the only difference between rounded dimensions + paddings\r\n // and \"client\" properties, though that is not always true in Chrome.\r\n var vertScrollbar = Math.round(width + horizPad) - clientWidth;\r\n var horizScrollbar = Math.round(height + vertPad) - clientHeight;\r\n // Chrome has a rather weird rounding of \"client\" properties.\r\n // E.g. for an element with content width of 314.2px it sometimes gives\r\n // the client width of 315px and for the width of 314.7px it may give\r\n // 314px. And it doesn't happen all the time. So just ignore this delta\r\n // as a non-relevant.\r\n if (Math.abs(vertScrollbar) !== 1) {\r\n width -= vertScrollbar;\r\n }\r\n if (Math.abs(horizScrollbar) !== 1) {\r\n height -= horizScrollbar;\r\n }\r\n }\r\n return createRectInit(paddings.left, paddings.top, width, height);\r\n}\r\n/**\r\n * Checks whether provided element is an instance of the SVGGraphicsElement.\r\n *\r\n * @param {Element} target - Element to be checked.\r\n * @returns {boolean}\r\n */\r\nvar isSVGGraphicsElement = (function () {\r\n // Some browsers, namely IE and Edge, don't have the SVGGraphicsElement\r\n // interface.\r\n if (typeof SVGGraphicsElement !== 'undefined') {\r\n return function (target) { return target instanceof getWindowOf(target).SVGGraphicsElement; };\r\n }\r\n // If it's so, then check that element is at least an instance of the\r\n // SVGElement and that it has the \"getBBox\" method.\r\n // eslint-disable-next-line no-extra-parens\r\n return function (target) { return (target instanceof getWindowOf(target).SVGElement &&\r\n typeof target.getBBox === 'function'); };\r\n})();\r\n/**\r\n * Checks whether provided element is a document element ().\r\n *\r\n * @param {Element} target - Element to be checked.\r\n * @returns {boolean}\r\n */\r\nfunction isDocumentElement(target) {\r\n return target === getWindowOf(target).document.documentElement;\r\n}\r\n/**\r\n * Calculates an appropriate content rectangle for provided html or svg element.\r\n *\r\n * @param {Element} target - Element content rectangle of which needs to be calculated.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction getContentRect(target) {\r\n if (!isBrowser) {\r\n return emptyRect;\r\n }\r\n if (isSVGGraphicsElement(target)) {\r\n return getSVGContentRect(target);\r\n }\r\n return getHTMLElementContentRect(target);\r\n}\r\n/**\r\n * Creates rectangle with an interface of the DOMRectReadOnly.\r\n * Spec: https://drafts.fxtf.org/geometry/#domrectreadonly\r\n *\r\n * @param {DOMRectInit} rectInit - Object with rectangle's x/y coordinates and dimensions.\r\n * @returns {DOMRectReadOnly}\r\n */\r\nfunction createReadOnlyRect(_a) {\r\n var x = _a.x, y = _a.y, width = _a.width, height = _a.height;\r\n // If DOMRectReadOnly is available use it as a prototype for the rectangle.\r\n var Constr = typeof DOMRectReadOnly !== 'undefined' ? DOMRectReadOnly : Object;\r\n var rect = Object.create(Constr.prototype);\r\n // Rectangle's properties are not writable and non-enumerable.\r\n defineConfigurable(rect, {\r\n x: x, y: y, width: width, height: height,\r\n top: y,\r\n right: x + width,\r\n bottom: height + y,\r\n left: x\r\n });\r\n return rect;\r\n}\r\n/**\r\n * Creates DOMRectInit object based on the provided dimensions and the x/y coordinates.\r\n * Spec: https://drafts.fxtf.org/geometry/#dictdef-domrectinit\r\n *\r\n * @param {number} x - X coordinate.\r\n * @param {number} y - Y coordinate.\r\n * @param {number} width - Rectangle's width.\r\n * @param {number} height - Rectangle's height.\r\n * @returns {DOMRectInit}\r\n */\r\nfunction createRectInit(x, y, width, height) {\r\n return { x: x, y: y, width: width, height: height };\r\n}\n\n/**\r\n * Class that is responsible for computations of the content rectangle of\r\n * provided DOM element and for keeping track of it's changes.\r\n */\r\nvar ResizeObservation = /** @class */ (function () {\r\n /**\r\n * Creates an instance of ResizeObservation.\r\n *\r\n * @param {Element} target - Element to be observed.\r\n */\r\n function ResizeObservation(target) {\r\n /**\r\n * Broadcasted width of content rectangle.\r\n *\r\n * @type {number}\r\n */\r\n this.broadcastWidth = 0;\r\n /**\r\n * Broadcasted height of content rectangle.\r\n *\r\n * @type {number}\r\n */\r\n this.broadcastHeight = 0;\r\n /**\r\n * Reference to the last observed content rectangle.\r\n *\r\n * @private {DOMRectInit}\r\n */\r\n this.contentRect_ = createRectInit(0, 0, 0, 0);\r\n this.target = target;\r\n }\r\n /**\r\n * Updates content rectangle and tells whether it's width or height properties\r\n * have changed since the last broadcast.\r\n *\r\n * @returns {boolean}\r\n */\r\n ResizeObservation.prototype.isActive = function () {\r\n var rect = getContentRect(this.target);\r\n this.contentRect_ = rect;\r\n return (rect.width !== this.broadcastWidth ||\r\n rect.height !== this.broadcastHeight);\r\n };\r\n /**\r\n * Updates 'broadcastWidth' and 'broadcastHeight' properties with a data\r\n * from the corresponding properties of the last observed content rectangle.\r\n *\r\n * @returns {DOMRectInit} Last observed content rectangle.\r\n */\r\n ResizeObservation.prototype.broadcastRect = function () {\r\n var rect = this.contentRect_;\r\n this.broadcastWidth = rect.width;\r\n this.broadcastHeight = rect.height;\r\n return rect;\r\n };\r\n return ResizeObservation;\r\n}());\n\nvar ResizeObserverEntry = /** @class */ (function () {\r\n /**\r\n * Creates an instance of ResizeObserverEntry.\r\n *\r\n * @param {Element} target - Element that is being observed.\r\n * @param {DOMRectInit} rectInit - Data of the element's content rectangle.\r\n */\r\n function ResizeObserverEntry(target, rectInit) {\r\n var contentRect = createReadOnlyRect(rectInit);\r\n // According to the specification following properties are not writable\r\n // and are also not enumerable in the native implementation.\r\n //\r\n // Property accessors are not being used as they'd require to define a\r\n // private WeakMap storage which may cause memory leaks in browsers that\r\n // don't support this type of collections.\r\n defineConfigurable(this, { target: target, contentRect: contentRect });\r\n }\r\n return ResizeObserverEntry;\r\n}());\n\nvar ResizeObserverSPI = /** @class */ (function () {\r\n /**\r\n * Creates a new instance of ResizeObserver.\r\n *\r\n * @param {ResizeObserverCallback} callback - Callback function that is invoked\r\n * when one of the observed elements changes it's content dimensions.\r\n * @param {ResizeObserverController} controller - Controller instance which\r\n * is responsible for the updates of observer.\r\n * @param {ResizeObserver} callbackCtx - Reference to the public\r\n * ResizeObserver instance which will be passed to callback function.\r\n */\r\n function ResizeObserverSPI(callback, controller, callbackCtx) {\r\n /**\r\n * Collection of resize observations that have detected changes in dimensions\r\n * of elements.\r\n *\r\n * @private {Array}\r\n */\r\n this.activeObservations_ = [];\r\n /**\r\n * Registry of the ResizeObservation instances.\r\n *\r\n * @private {Map}\r\n */\r\n this.observations_ = new MapShim();\r\n if (typeof callback !== 'function') {\r\n throw new TypeError('The callback provided as parameter 1 is not a function.');\r\n }\r\n this.callback_ = callback;\r\n this.controller_ = controller;\r\n this.callbackCtx_ = callbackCtx;\r\n }\r\n /**\r\n * Starts observing provided element.\r\n *\r\n * @param {Element} target - Element to be observed.\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.observe = function (target) {\r\n if (!arguments.length) {\r\n throw new TypeError('1 argument required, but only 0 present.');\r\n }\r\n // Do nothing if current environment doesn't have the Element interface.\r\n if (typeof Element === 'undefined' || !(Element instanceof Object)) {\r\n return;\r\n }\r\n if (!(target instanceof getWindowOf(target).Element)) {\r\n throw new TypeError('parameter 1 is not of type \"Element\".');\r\n }\r\n var observations = this.observations_;\r\n // Do nothing if element is already being observed.\r\n if (observations.has(target)) {\r\n return;\r\n }\r\n observations.set(target, new ResizeObservation(target));\r\n this.controller_.addObserver(this);\r\n // Force the update of observations.\r\n this.controller_.refresh();\r\n };\r\n /**\r\n * Stops observing provided element.\r\n *\r\n * @param {Element} target - Element to stop observing.\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.unobserve = function (target) {\r\n if (!arguments.length) {\r\n throw new TypeError('1 argument required, but only 0 present.');\r\n }\r\n // Do nothing if current environment doesn't have the Element interface.\r\n if (typeof Element === 'undefined' || !(Element instanceof Object)) {\r\n return;\r\n }\r\n if (!(target instanceof getWindowOf(target).Element)) {\r\n throw new TypeError('parameter 1 is not of type \"Element\".');\r\n }\r\n var observations = this.observations_;\r\n // Do nothing if element is not being observed.\r\n if (!observations.has(target)) {\r\n return;\r\n }\r\n observations.delete(target);\r\n if (!observations.size) {\r\n this.controller_.removeObserver(this);\r\n }\r\n };\r\n /**\r\n * Stops observing all elements.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.disconnect = function () {\r\n this.clearActive();\r\n this.observations_.clear();\r\n this.controller_.removeObserver(this);\r\n };\r\n /**\r\n * Collects observation instances the associated element of which has changed\r\n * it's content rectangle.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.gatherActive = function () {\r\n var _this = this;\r\n this.clearActive();\r\n this.observations_.forEach(function (observation) {\r\n if (observation.isActive()) {\r\n _this.activeObservations_.push(observation);\r\n }\r\n });\r\n };\r\n /**\r\n * Invokes initial callback function with a list of ResizeObserverEntry\r\n * instances collected from active resize observations.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.broadcastActive = function () {\r\n // Do nothing if observer doesn't have active observations.\r\n if (!this.hasActive()) {\r\n return;\r\n }\r\n var ctx = this.callbackCtx_;\r\n // Create ResizeObserverEntry instance for every active observation.\r\n var entries = this.activeObservations_.map(function (observation) {\r\n return new ResizeObserverEntry(observation.target, observation.broadcastRect());\r\n });\r\n this.callback_.call(ctx, entries, ctx);\r\n this.clearActive();\r\n };\r\n /**\r\n * Clears the collection of active observations.\r\n *\r\n * @returns {void}\r\n */\r\n ResizeObserverSPI.prototype.clearActive = function () {\r\n this.activeObservations_.splice(0);\r\n };\r\n /**\r\n * Tells whether observer has active observations.\r\n *\r\n * @returns {boolean}\r\n */\r\n ResizeObserverSPI.prototype.hasActive = function () {\r\n return this.activeObservations_.length > 0;\r\n };\r\n return ResizeObserverSPI;\r\n}());\n\n// Registry of internal observers. If WeakMap is not available use current shim\r\n// for the Map collection as it has all required methods and because WeakMap\r\n// can't be fully polyfilled anyway.\r\nvar observers = typeof WeakMap !== 'undefined' ? new WeakMap() : new MapShim();\r\n/**\r\n * ResizeObserver API. Encapsulates the ResizeObserver SPI implementation\r\n * exposing only those methods and properties that are defined in the spec.\r\n */\r\nvar ResizeObserver = /** @class */ (function () {\r\n /**\r\n * Creates a new instance of ResizeObserver.\r\n *\r\n * @param {ResizeObserverCallback} callback - Callback that is invoked when\r\n * dimensions of the observed elements change.\r\n */\r\n function ResizeObserver(callback) {\r\n if (!(this instanceof ResizeObserver)) {\r\n throw new TypeError('Cannot call a class as a function.');\r\n }\r\n if (!arguments.length) {\r\n throw new TypeError('1 argument required, but only 0 present.');\r\n }\r\n var controller = ResizeObserverController.getInstance();\r\n var observer = new ResizeObserverSPI(callback, controller, this);\r\n observers.set(this, observer);\r\n }\r\n return ResizeObserver;\r\n}());\r\n// Expose public methods of ResizeObserver.\r\n[\r\n 'observe',\r\n 'unobserve',\r\n 'disconnect'\r\n].forEach(function (method) {\r\n ResizeObserver.prototype[method] = function () {\r\n var _a;\r\n return (_a = observers.get(this))[method].apply(_a, arguments);\r\n };\r\n});\n\nvar index = (function () {\r\n // Export existing implementation if available.\r\n if (typeof global$1.ResizeObserver !== 'undefined') {\r\n return global$1.ResizeObserver;\r\n }\r\n return ResizeObserver;\r\n})();\n\nexport default index;\n","import { Observable } from '../Observable';\nimport { from } from './from';\nexport function defer(observableFactory) {\n return new Observable(subscriber => {\n let input;\n try {\n input = observableFactory();\n }\n catch (err) {\n subscriber.error(err);\n return undefined;\n }\n const source = from(input);\n return source.subscribe(subscriber);\n });\n}\n//# sourceMappingURL=defer.js.map","/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n","import { operate } from '../util/lift';\nexport function finalize(callback) {\n return operate((source, subscriber) => {\n source.subscribe(subscriber);\n subscriber.add(callback);\n });\n}\n//# sourceMappingURL=finalize.js.map","import { isFunction } from '../util/isFunction';\nimport { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nimport { identity } from '../util/identity';\nexport function tap(observerOrNext, error, complete) {\n const tapObserver = isFunction(observerOrNext) || error || complete ? { next: observerOrNext, error, complete } : observerOrNext;\n return tapObserver\n ? operate((source, subscriber) => {\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n var _a;\n (_a = tapObserver.next) === null || _a === void 0 ? void 0 : _a.call(tapObserver, value);\n subscriber.next(value);\n }, (err) => {\n var _a;\n (_a = tapObserver.error) === null || _a === void 0 ? void 0 : _a.call(tapObserver, err);\n subscriber.error(err);\n }, () => {\n var _a;\n (_a = tapObserver.complete) === null || _a === void 0 ? void 0 : _a.call(tapObserver);\n subscriber.complete();\n }));\n })\n :\n identity;\n}\n//# sourceMappingURL=tap.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function scan(accumulator, seed) {\n const hasSeed = arguments.length >= 2;\n return operate((source, subscriber) => {\n let hasState = hasSeed;\n let state = seed;\n let index = 0;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n const i = index++;\n subscriber.next((state = hasState\n ?\n accumulator(state, value, i)\n :\n ((hasState = true), value)));\n }));\n });\n}\n//# sourceMappingURL=scan.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function observeOn(scheduler, delay = 0) {\n return operate((source, subscriber) => {\n source.subscribe(new OperatorSubscriber(subscriber, (value) => subscriber.add(scheduler.schedule(() => subscriber.next(value), delay)), (err) => subscriber.add(scheduler.schedule(() => subscriber.error(err), delay)), () => subscriber.add(scheduler.schedule(() => subscriber.complete(), delay))));\n });\n}\n//# sourceMappingURL=observeOn.js.map","import { Subscription } from '../Subscription';\nexport const animationFrameProvider = {\n schedule(callback) {\n let request = requestAnimationFrame;\n let cancel = cancelAnimationFrame;\n const { delegate } = animationFrameProvider;\n if (delegate) {\n request = delegate.requestAnimationFrame;\n cancel = delegate.cancelAnimationFrame;\n }\n const handle = request((timestamp) => {\n cancel = undefined;\n callback(timestamp);\n });\n return new Subscription(() => cancel === null || cancel === void 0 ? void 0 : cancel(handle));\n },\n requestAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return ((delegate === null || delegate === void 0 ? void 0 : delegate.requestAnimationFrame) || requestAnimationFrame)(...args);\n },\n cancelAnimationFrame(...args) {\n const { delegate } = animationFrameProvider;\n return ((delegate === null || delegate === void 0 ? void 0 : delegate.cancelAnimationFrame) || cancelAnimationFrame)(...args);\n },\n delegate: undefined,\n};\n//# sourceMappingURL=animationFrameProvider.js.map","import { AsyncAction } from './AsyncAction';\nimport { animationFrameProvider } from './animationFrameProvider';\nexport class AnimationFrameAction extends AsyncAction {\n constructor(scheduler, work) {\n super(scheduler, work);\n this.scheduler = scheduler;\n this.work = work;\n }\n requestAsyncId(scheduler, id, delay = 0) {\n if (delay !== null && delay > 0) {\n return super.requestAsyncId(scheduler, id, delay);\n }\n scheduler.actions.push(this);\n return scheduler.scheduled || (scheduler.scheduled = animationFrameProvider.requestAnimationFrame(() => scheduler.flush(undefined)));\n }\n recycleAsyncId(scheduler, id, delay = 0) {\n if ((delay != null && delay > 0) || (delay == null && this.delay > 0)) {\n return super.recycleAsyncId(scheduler, id, delay);\n }\n if (scheduler.actions.length === 0) {\n animationFrameProvider.cancelAnimationFrame(id);\n scheduler.scheduled = undefined;\n }\n return undefined;\n }\n}\n//# sourceMappingURL=AnimationFrameAction.js.map","import { AsyncScheduler } from './AsyncScheduler';\nexport class AnimationFrameScheduler extends AsyncScheduler {\n flush(action) {\n this.active = true;\n this.scheduled = undefined;\n const { actions } = this;\n let error;\n let index = -1;\n action = action || actions.shift();\n let count = actions.length;\n do {\n if (error = action.execute(action.state, action.delay)) {\n break;\n }\n } while (++index < count && (action = actions.shift()));\n this.active = false;\n if (error) {\n while (++index < count && (action = actions.shift())) {\n action.unsubscribe();\n }\n throw error;\n }\n }\n}\n//# sourceMappingURL=AnimationFrameScheduler.js.map","import { AnimationFrameAction } from './AnimationFrameAction';\nimport { AnimationFrameScheduler } from './AnimationFrameScheduler';\nexport const animationFrameScheduler = new AnimationFrameScheduler(AnimationFrameAction);\nexport const animationFrame = animationFrameScheduler;\n//# sourceMappingURL=animationFrame.js.map","(function (global, factory) {\n typeof exports === 'object' && typeof module !== 'undefined' ? factory() :\n typeof define === 'function' && define.amd ? define(factory) :\n (factory());\n}(this, (function () { 'use strict';\n\n /**\n * Applies the :focus-visible polyfill at the given scope.\n * A scope in this case is either the top-level Document or a Shadow Root.\n *\n * @param {(Document|ShadowRoot)} scope\n * @see https://github.com/WICG/focus-visible\n */\n function applyFocusVisiblePolyfill(scope) {\n var hadKeyboardEvent = true;\n var hadFocusVisibleRecently = false;\n var hadFocusVisibleRecentlyTimeout = null;\n\n var inputTypesWhitelist = {\n text: true,\n search: true,\n url: true,\n tel: true,\n email: true,\n password: true,\n number: true,\n date: true,\n month: true,\n week: true,\n time: true,\n datetime: true,\n 'datetime-local': true\n };\n\n /**\n * Helper function for legacy browsers and iframes which sometimes focus\n * elements like document, body, and non-interactive SVG.\n * @param {Element} el\n */\n function isValidFocusTarget(el) {\n if (\n el &&\n el !== document &&\n el.nodeName !== 'HTML' &&\n el.nodeName !== 'BODY' &&\n 'classList' in el &&\n 'contains' in el.classList\n ) {\n return true;\n }\n return false;\n }\n\n /**\n * Computes whether the given element should automatically trigger the\n * `focus-visible` class being added, i.e. whether it should always match\n * `:focus-visible` when focused.\n * @param {Element} el\n * @return {boolean}\n */\n function focusTriggersKeyboardModality(el) {\n var type = el.type;\n var tagName = el.tagName;\n\n if (tagName === 'INPUT' && inputTypesWhitelist[type] && !el.readOnly) {\n return true;\n }\n\n if (tagName === 'TEXTAREA' && !el.readOnly) {\n return true;\n }\n\n if (el.isContentEditable) {\n return true;\n }\n\n return false;\n }\n\n /**\n * Add the `focus-visible` class to the given element if it was not added by\n * the author.\n * @param {Element} el\n */\n function addFocusVisibleClass(el) {\n if (el.classList.contains('focus-visible')) {\n return;\n }\n el.classList.add('focus-visible');\n el.setAttribute('data-focus-visible-added', '');\n }\n\n /**\n * Remove the `focus-visible` class from the given element if it was not\n * originally added by the author.\n * @param {Element} el\n */\n function removeFocusVisibleClass(el) {\n if (!el.hasAttribute('data-focus-visible-added')) {\n return;\n }\n el.classList.remove('focus-visible');\n el.removeAttribute('data-focus-visible-added');\n }\n\n /**\n * If the most recent user interaction was via the keyboard;\n * and the key press did not include a meta, alt/option, or control key;\n * then the modality is keyboard. Otherwise, the modality is not keyboard.\n * Apply `focus-visible` to any current active element and keep track\n * of our keyboard modality state with `hadKeyboardEvent`.\n * @param {KeyboardEvent} e\n */\n function onKeyDown(e) {\n if (e.metaKey || e.altKey || e.ctrlKey) {\n return;\n }\n\n if (isValidFocusTarget(scope.activeElement)) {\n addFocusVisibleClass(scope.activeElement);\n }\n\n hadKeyboardEvent = true;\n }\n\n /**\n * If at any point a user clicks with a pointing device, ensure that we change\n * the modality away from keyboard.\n * This avoids the situation where a user presses a key on an already focused\n * element, and then clicks on a different element, focusing it with a\n * pointing device, while we still think we're in keyboard modality.\n * @param {Event} e\n */\n function onPointerDown(e) {\n hadKeyboardEvent = false;\n }\n\n /**\n * On `focus`, add the `focus-visible` class to the target if:\n * - the target received focus as a result of keyboard navigation, or\n * - the event target is an element that will likely require interaction\n * via the keyboard (e.g. a text box)\n * @param {Event} e\n */\n function onFocus(e) {\n // Prevent IE from focusing the document or HTML element.\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (hadKeyboardEvent || focusTriggersKeyboardModality(e.target)) {\n addFocusVisibleClass(e.target);\n }\n }\n\n /**\n * On `blur`, remove the `focus-visible` class from the target.\n * @param {Event} e\n */\n function onBlur(e) {\n if (!isValidFocusTarget(e.target)) {\n return;\n }\n\n if (\n e.target.classList.contains('focus-visible') ||\n e.target.hasAttribute('data-focus-visible-added')\n ) {\n // To detect a tab/window switch, we look for a blur event followed\n // rapidly by a visibility change.\n // If we don't see a visibility change within 100ms, it's probably a\n // regular focus change.\n hadFocusVisibleRecently = true;\n window.clearTimeout(hadFocusVisibleRecentlyTimeout);\n hadFocusVisibleRecentlyTimeout = window.setTimeout(function() {\n hadFocusVisibleRecently = false;\n }, 100);\n removeFocusVisibleClass(e.target);\n }\n }\n\n /**\n * If the user changes tabs, keep track of whether or not the previously\n * focused element had .focus-visible.\n * @param {Event} e\n */\n function onVisibilityChange(e) {\n if (document.visibilityState === 'hidden') {\n // If the tab becomes active again, the browser will handle calling focus\n // on the element (Safari actually calls it twice).\n // If this tab change caused a blur on an element with focus-visible,\n // re-apply the class when the user switches back to the tab.\n if (hadFocusVisibleRecently) {\n hadKeyboardEvent = true;\n }\n addInitialPointerMoveListeners();\n }\n }\n\n /**\n * Add a group of listeners to detect usage of any pointing devices.\n * These listeners will be added when the polyfill first loads, and anytime\n * the window is blurred, so that they are active when the window regains\n * focus.\n */\n function addInitialPointerMoveListeners() {\n document.addEventListener('mousemove', onInitialPointerMove);\n document.addEventListener('mousedown', onInitialPointerMove);\n document.addEventListener('mouseup', onInitialPointerMove);\n document.addEventListener('pointermove', onInitialPointerMove);\n document.addEventListener('pointerdown', onInitialPointerMove);\n document.addEventListener('pointerup', onInitialPointerMove);\n document.addEventListener('touchmove', onInitialPointerMove);\n document.addEventListener('touchstart', onInitialPointerMove);\n document.addEventListener('touchend', onInitialPointerMove);\n }\n\n function removeInitialPointerMoveListeners() {\n document.removeEventListener('mousemove', onInitialPointerMove);\n document.removeEventListener('mousedown', onInitialPointerMove);\n document.removeEventListener('mouseup', onInitialPointerMove);\n document.removeEventListener('pointermove', onInitialPointerMove);\n document.removeEventListener('pointerdown', onInitialPointerMove);\n document.removeEventListener('pointerup', onInitialPointerMove);\n document.removeEventListener('touchmove', onInitialPointerMove);\n document.removeEventListener('touchstart', onInitialPointerMove);\n document.removeEventListener('touchend', onInitialPointerMove);\n }\n\n /**\n * When the polfyill first loads, assume the user is in keyboard modality.\n * If any event is received from a pointing device (e.g. mouse, pointer,\n * touch), turn off keyboard modality.\n * This accounts for situations where focus enters the page from the URL bar.\n * @param {Event} e\n */\n function onInitialPointerMove(e) {\n // Work around a Safari quirk that fires a mousemove on whenever the\n // window blurs, even if you're tabbing out of the page. ¯\\_(ツ)_/¯\n if (e.target.nodeName && e.target.nodeName.toLowerCase() === 'html') {\n return;\n }\n\n hadKeyboardEvent = false;\n removeInitialPointerMoveListeners();\n }\n\n // For some kinds of state, we are interested in changes at the global scope\n // only. For example, global pointer input, global key presses and global\n // visibility change should affect the state at every scope:\n document.addEventListener('keydown', onKeyDown, true);\n document.addEventListener('mousedown', onPointerDown, true);\n document.addEventListener('pointerdown', onPointerDown, true);\n document.addEventListener('touchstart', onPointerDown, true);\n document.addEventListener('visibilitychange', onVisibilityChange, true);\n\n addInitialPointerMoveListeners();\n\n // For focus and blur, we specifically care about state changes in the local\n // scope. This is because focus / blur events that originate from within a\n // shadow root are not re-dispatched from the host element if it was already\n // the active element in its own scope:\n scope.addEventListener('focus', onFocus, true);\n scope.addEventListener('blur', onBlur, true);\n\n // We detect that a node is a ShadowRoot by ensuring that it is a\n // DocumentFragment and also has a host property. This check covers native\n // implementation and polyfill implementation transparently. If we only cared\n // about the native implementation, we could just check if the scope was\n // an instance of a ShadowRoot.\n if (scope.nodeType === Node.DOCUMENT_FRAGMENT_NODE && scope.host) {\n // Since a ShadowRoot is a special kind of DocumentFragment, it does not\n // have a root element to add a class to. So, we add this attribute to the\n // host element instead:\n scope.host.setAttribute('data-js-focus-visible', '');\n } else if (scope.nodeType === Node.DOCUMENT_NODE) {\n document.documentElement.classList.add('js-focus-visible');\n document.documentElement.setAttribute('data-js-focus-visible', '');\n }\n }\n\n // It is important to wrap all references to global window and document in\n // these checks to support server-side rendering use cases\n // @see https://github.com/WICG/focus-visible/issues/199\n if (typeof window !== 'undefined' && typeof document !== 'undefined') {\n // Make the polyfill helper globally available. This can be used as a signal\n // to interested libraries that wish to coordinate with the polyfill for e.g.,\n // applying the polyfill to a shadow root:\n window.applyFocusVisiblePolyfill = applyFocusVisiblePolyfill;\n\n // Notify interested libraries of the polyfill's presence, in case the\n // polyfill was loaded lazily:\n var event;\n\n try {\n event = new CustomEvent('focus-visible-polyfill-ready');\n } catch (error) {\n // IE11 does not support using CustomEvent as a constructor directly:\n event = document.createEvent('CustomEvent');\n event.initCustomEvent('focus-visible-polyfill-ready', false, false, {});\n }\n\n window.dispatchEvent(event);\n }\n\n if (typeof document !== 'undefined') {\n // Apply the polyfill to the global document, so that no JavaScript\n // coordination is required to use the polyfill in the top-level document:\n applyFocusVisiblePolyfill(document);\n }\n\n})));\n","var g;\n\n// This works in non-strict mode\ng = (function() {\n\treturn this;\n})();\n\ntry {\n\t// This works if eval is allowed (see CSP)\n\tg = g || new Function(\"return this\")();\n} catch (e) {\n\t// This works if the window reference is available\n\tif (typeof window === \"object\") g = window;\n}\n\n// g can still be undefined, but nothing to do about it...\n// We return undefined, instead of nothing here, so it's\n// easier to handle this case. if(!global) { ...}\n\nmodule.exports = g;\n","import { ReplaySubject } from '../ReplaySubject';\nimport { operate } from '../util/lift';\nexport function shareReplay(configOrBufferSize, windowTime, scheduler) {\n let config;\n if (configOrBufferSize && typeof configOrBufferSize === 'object') {\n config = configOrBufferSize;\n }\n else {\n config = {\n bufferSize: configOrBufferSize,\n windowTime,\n refCount: false,\n scheduler\n };\n }\n return operate(shareReplayOperator(config));\n}\nfunction shareReplayOperator({ bufferSize = Infinity, windowTime = Infinity, refCount: useRefCount, scheduler }) {\n let subject;\n let refCount = 0;\n let subscription;\n return (source, subscriber) => {\n refCount++;\n let innerSub;\n if (!subject) {\n subject = new ReplaySubject(bufferSize, windowTime, scheduler);\n innerSub = subject.subscribe(subscriber);\n subscription = source.subscribe({\n next(value) { subject.next(value); },\n error(err) {\n const dest = subject;\n subscription = undefined;\n subject = undefined;\n dest.error(err);\n },\n complete() {\n subscription = undefined;\n subject.complete();\n },\n });\n if (subscription.closed) {\n subscription = undefined;\n }\n }\n else {\n innerSub = subject.subscribe(subscriber);\n }\n subscriber.add(() => {\n refCount--;\n innerSub.unsubscribe();\n if (useRefCount && refCount === 0 && subscription) {\n subscription.unsubscribe();\n subscription = undefined;\n subject = undefined;\n }\n });\n };\n}\n//# sourceMappingURL=shareReplay.js.map","import { distinctUntilChanged } from './distinctUntilChanged';\nexport function distinctUntilKeyChanged(key, compare) {\n return distinctUntilChanged((x, y) => compare ? compare(x[key], y[key]) : x[key] === y[key]);\n}\n//# sourceMappingURL=distinctUntilKeyChanged.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nimport { from } from '../observable/from';\nimport { identity } from '../util/identity';\nimport { noop } from '../util/noop';\nexport function withLatestFrom(...inputs) {\n let project;\n if (typeof inputs[inputs.length - 1] === 'function') {\n project = inputs.pop();\n }\n return operate((source, subscriber) => {\n const len = inputs.length;\n const otherValues = new Array(len);\n let hasValue = inputs.map(() => false);\n let ready = false;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n if (ready) {\n const values = [value, ...otherValues];\n subscriber.next(project ? project(...values) : values);\n }\n }));\n for (let i = 0; i < len; i++) {\n const input = inputs[i];\n let otherSource;\n try {\n otherSource = from(input);\n }\n catch (err) {\n subscriber.error(err);\n return;\n }\n otherSource.subscribe(new OperatorSubscriber(subscriber, (value) => {\n otherValues[i] = value;\n if (!ready && !hasValue[i]) {\n hasValue[i] = true;\n (ready = hasValue.every(identity)) && (hasValue = null);\n }\n }, undefined, noop));\n }\n });\n}\n//# sourceMappingURL=withLatestFrom.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nimport { arrRemove } from '../util/arrRemove';\nexport function bufferCount(bufferSize, startBufferEvery = null) {\n startBufferEvery = startBufferEvery !== null && startBufferEvery !== void 0 ? startBufferEvery : bufferSize;\n return operate((source, subscriber) => {\n let buffers = [];\n let count = 0;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n let toEmit = null;\n if (count++ % startBufferEvery === 0) {\n buffers.push([]);\n }\n for (const buffer of buffers) {\n buffer.push(value);\n if (bufferSize <= buffer.length) {\n toEmit = toEmit !== null && toEmit !== void 0 ? toEmit : [];\n toEmit.push(buffer);\n }\n }\n if (toEmit) {\n for (const buffer of toEmit) {\n arrRemove(buffers, buffer);\n subscriber.next(buffer);\n }\n }\n }, undefined, () => {\n for (const buffer of buffers) {\n subscriber.next(buffer);\n }\n subscriber.complete();\n }, () => {\n buffers = null;\n }));\n });\n}\n//# sourceMappingURL=bufferCount.js.map","import { concatAll } from '../operators/concatAll';\nimport { isScheduler } from '../util/isScheduler';\nimport { fromArray } from './fromArray';\nexport function concat(...args) {\n let scheduler;\n if (isScheduler(args[args.length - 1])) {\n scheduler = args.pop();\n }\n return concatAll()(fromArray(args, scheduler));\n}\n//# sourceMappingURL=concat.js.map","import { mergeAll } from './mergeAll';\nexport function concatAll() {\n return mergeAll(1);\n}\n//# sourceMappingURL=concatAll.js.map","import { concat } from '../observable/concat';\nimport { isScheduler } from '../util/isScheduler';\nexport function startWith(...values) {\n const scheduler = values[values.length - 1];\n if (isScheduler(scheduler)) {\n values.pop();\n return (source) => concat(values, source, scheduler);\n }\n else {\n return (source) => concat(values, source);\n }\n}\n//# sourceMappingURL=startWith.js.map","import { Observable } from '../Observable';\nimport { mergeMap } from '../operators/mergeMap';\nimport { isArrayLike } from '../util/isArrayLike';\nimport { isFunction } from '../util/isFunction';\nimport { mapOneOrManyArgs } from '../util/mapOneOrManyArgs';\nimport { fromArray } from './fromArray';\nexport function fromEvent(target, eventName, options, resultSelector) {\n if (isFunction(options)) {\n resultSelector = options;\n options = undefined;\n }\n if (resultSelector) {\n return fromEvent(target, eventName, options).pipe(mapOneOrManyArgs(resultSelector));\n }\n return new Observable((subscriber) => {\n const handler = (...args) => subscriber.next(args.length > 1 ? args : args[0]);\n if (isEventTarget(target)) {\n target.addEventListener(eventName, handler, options);\n return () => target.removeEventListener(eventName, handler, options);\n }\n if (isJQueryStyleEventEmitter(target)) {\n target.on(eventName, handler);\n return () => target.off(eventName, handler);\n }\n if (isNodeStyleEventEmitter(target)) {\n target.addListener(eventName, handler);\n return () => target.removeListener(eventName, handler);\n }\n if (isArrayLike(target)) {\n return mergeMap((target) => fromEvent(target, eventName, options))(fromArray(target)).subscribe(subscriber);\n }\n subscriber.error(new TypeError('Invalid event target'));\n return;\n });\n}\nfunction isNodeStyleEventEmitter(sourceObj) {\n return sourceObj && typeof sourceObj.addListener === 'function' && typeof sourceObj.removeListener === 'function';\n}\nfunction isJQueryStyleEventEmitter(sourceObj) {\n return sourceObj && typeof sourceObj.on === 'function' && typeof sourceObj.off === 'function';\n}\nfunction isEventTarget(sourceObj) {\n return sourceObj && typeof sourceObj.addEventListener === 'function' && typeof sourceObj.removeEventListener === 'function';\n}\n//# sourceMappingURL=fromEvent.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function mapTo(value) {\n return operate((source, subscriber) => {\n source.subscribe(new OperatorSubscriber(subscriber, () => subscriber.next(value)));\n });\n}\n//# sourceMappingURL=mapTo.js.map","import { Observable } from '../Observable';\nimport { noop } from '../util/noop';\nexport const NEVER = new Observable(noop);\nexport function never() {\n return NEVER;\n}\n//# sourceMappingURL=never.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function filter(predicate, thisArg) {\n return operate((source, subscriber) => {\n let index = 0;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => predicate.call(thisArg, value, index++) && subscriber.next(value)));\n });\n}\n//# sourceMappingURL=filter.js.map","import { Subject } from './Subject';\nexport class BehaviorSubject extends Subject {\n constructor(_value) {\n super();\n this._value = _value;\n }\n get value() {\n return this.getValue();\n }\n _subscribe(subscriber) {\n const subscription = super._subscribe(subscriber);\n !subscription.closed && subscriber.next(this._value);\n return subscription;\n }\n getValue() {\n const { hasError, thrownError, _value } = this;\n if (hasError) {\n throw thrownError;\n }\n this._throwIfClosed();\n return _value;\n }\n next(value) {\n super.next((this._value = value));\n }\n}\n//# sourceMappingURL=BehaviorSubject.js.map","import { EMPTY } from '../observable/empty';\nimport { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function take(count) {\n return count <= 0\n ? () => EMPTY\n : operate((source, subscriber) => {\n let seen = 0;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n if (++seen <= count) {\n subscriber.next(value);\n if (count <= seen) {\n subscriber.complete();\n }\n }\n }));\n });\n}\n//# sourceMappingURL=take.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nimport { from } from '../observable/from';\nexport const defaultThrottleConfig = {\n leading: true,\n trailing: false,\n};\nexport function throttle(durationSelector, { leading, trailing } = defaultThrottleConfig) {\n return operate((source, subscriber) => {\n let hasValue = false;\n let sendValue = null;\n let throttled = null;\n const throttlingDone = () => {\n throttled === null || throttled === void 0 ? void 0 : throttled.unsubscribe();\n throttled = null;\n trailing && send();\n };\n const throttle = (value) => (throttled = from(durationSelector(value)).subscribe(new OperatorSubscriber(subscriber, throttlingDone, undefined, throttlingDone)));\n const send = () => {\n if (hasValue) {\n subscriber.next(sendValue);\n throttle(sendValue);\n }\n hasValue = false;\n sendValue = null;\n };\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n hasValue = true;\n sendValue = value;\n !throttled && (leading ? send() : throttle(value));\n }));\n });\n}\n//# sourceMappingURL=throttle.js.map","import { switchMap } from './switchMap';\nexport function switchMapTo(innerObservable, resultSelector) {\n return resultSelector ? switchMap(() => innerObservable, resultSelector) : switchMap(() => innerObservable);\n}\n//# sourceMappingURL=switchMapTo.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function sample(notifier) {\n return operate((source, subscriber) => {\n let hasValue = false;\n let lastValue = null;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n hasValue = true;\n lastValue = value;\n }));\n const emit = () => {\n if (hasValue) {\n hasValue = false;\n const value = lastValue;\n lastValue = null;\n subscriber.next(value);\n }\n };\n notifier.subscribe(new OperatorSubscriber(subscriber, emit, undefined, emit));\n });\n}\n//# sourceMappingURL=sample.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function skip(count) {\n return operate((source, subscriber) => {\n let seen = 0;\n source.subscribe(new OperatorSubscriber(subscriber, (value) => (count === seen ? subscriber.next(value) : seen++)));\n });\n}\n//# sourceMappingURL=skip.js.map","import { from } from '../observable/from';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nimport { operate } from '../util/lift';\nexport function catchError(selector) {\n return operate((source, subscriber) => {\n let innerSub = null;\n let syncUnsub = false;\n let handledResult;\n innerSub = source.subscribe(new OperatorSubscriber(subscriber, undefined, (err) => {\n handledResult = from(selector(err, catchError(selector)(source)));\n if (innerSub) {\n innerSub.unsubscribe();\n innerSub = null;\n handledResult.subscribe(subscriber);\n }\n else {\n syncUnsub = true;\n }\n }));\n if (syncUnsub) {\n innerSub.unsubscribe();\n innerSub = null;\n handledResult.subscribe(subscriber);\n }\n });\n}\n//# sourceMappingURL=catchError.js.map","import { asyncScheduler } from '../scheduler/async';\nimport { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function debounceTime(dueTime, scheduler = asyncScheduler) {\n return operate((source, subscriber) => {\n let hasValue = false;\n let lastValue = null;\n let debounceSubscription = null;\n const emitLastValue = () => {\n hasValue = false;\n const value = lastValue;\n lastValue = null;\n subscriber.next(value);\n };\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n debounceSubscription === null || debounceSubscription === void 0 ? void 0 : debounceSubscription.unsubscribe();\n hasValue = true;\n lastValue = value;\n subscriber.add((debounceSubscription = scheduler.schedule(() => {\n debounceSubscription = null;\n emitLastValue();\n }, dueTime)));\n }, undefined, () => {\n hasValue && emitLastValue();\n subscriber.complete();\n }));\n });\n}\n//# sourceMappingURL=debounceTime.js.map","import { mergeMap } from './mergeMap';\nexport function concatMap(project, resultSelector) {\n if (typeof resultSelector === 'function') {\n return mergeMap(project, resultSelector, 1);\n }\n return mergeMap(project, 1);\n}\n//# sourceMappingURL=concatMap.js.map","import { defer } from './defer';\nimport { EMPTY } from './empty';\nexport function iif(condition, trueResult = EMPTY, falseResult = EMPTY) {\n return defer(() => condition() ? trueResult : falseResult);\n}\n//# sourceMappingURL=iif.js.map","import { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function refCount() {\n return operate((source, subscriber) => {\n let connection = null;\n source._refCount++;\n const refCounter = new OperatorSubscriber(subscriber, undefined, undefined, undefined, () => {\n if (!source || source._refCount <= 0 || 0 < --source._refCount) {\n connection = null;\n return;\n }\n const sharedConnection = source._connection;\n const conn = connection;\n connection = null;\n if (sharedConnection && (!conn || sharedConnection === conn)) {\n sharedConnection.unsubscribe();\n }\n subscriber.unsubscribe();\n });\n source.subscribe(refCounter);\n if (!refCounter.closed) {\n connection = source.connect();\n }\n });\n}\n//# sourceMappingURL=refCount.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nimport { refCount as higherOrderRefCount } from '../operators/refCount';\nimport { OperatorSubscriber } from '../operators/OperatorSubscriber';\nexport class ConnectableObservable extends Observable {\n constructor(source, subjectFactory) {\n super();\n this.source = source;\n this.subjectFactory = subjectFactory;\n this._subject = null;\n this._refCount = 0;\n this._connection = null;\n }\n _subscribe(subscriber) {\n return this.getSubject().subscribe(subscriber);\n }\n getSubject() {\n const subject = this._subject;\n if (!subject || subject.isStopped) {\n this._subject = this.subjectFactory();\n }\n return this._subject;\n }\n _teardown() {\n this._refCount = 0;\n const { _connection } = this;\n this._subject = this._connection = null;\n _connection === null || _connection === void 0 ? void 0 : _connection.unsubscribe();\n }\n connect() {\n let connection = this._connection;\n if (!connection) {\n connection = this._connection = new Subscription();\n const subject = this.getSubject();\n connection.add(this.source.subscribe(new OperatorSubscriber(subject, undefined, (err) => {\n this._teardown();\n subject.error(err);\n }, () => {\n this._teardown();\n subject.complete();\n }, () => this._teardown())));\n if (connection.closed) {\n this._connection = null;\n connection = Subscription.EMPTY;\n }\n }\n return connection;\n }\n refCount() {\n return higherOrderRefCount()(this);\n }\n}\n//# sourceMappingURL=ConnectableObservable.js.map","import { multicast } from './multicast';\nimport { refCount } from './refCount';\nimport { Subject } from '../Subject';\nfunction shareSubjectFactory() {\n return new Subject();\n}\nexport function share() {\n return (source) => refCount()(multicast(shareSubjectFactory)(source));\n}\n//# sourceMappingURL=share.js.map","import { ConnectableObservable } from '../observable/ConnectableObservable';\nimport { hasLift, operate } from '../util/lift';\nexport function multicast(subjectOrSubjectFactory, selector) {\n const subjectFactory = typeof subjectOrSubjectFactory === 'function' ? subjectOrSubjectFactory : () => subjectOrSubjectFactory;\n if (typeof selector === 'function') {\n return operate((source, subscriber) => {\n const subject = subjectFactory();\n selector(subject).subscribe(subscriber).add(source.subscribe(subject));\n });\n }\n return (source) => {\n const connectable = new ConnectableObservable(source, subjectFactory);\n if (hasLift(source)) {\n connectable.lift = source.lift;\n }\n connectable.source = source;\n connectable.subjectFactory = subjectFactory;\n return connectable;\n };\n}\n//# sourceMappingURL=multicast.js.map","import { zip as zipStatic } from '../observable/zip';\nimport { operate } from '../util/lift';\nexport function zip(...sources) {\n return operate((source, subscriber) => {\n zipStatic(source, ...sources).subscribe(subscriber);\n });\n}\nexport function zipWith(...otherInputs) {\n return zip(...otherInputs);\n}\n//# sourceMappingURL=zipWith.js.map","import { Observable } from '../Observable';\nimport { Subscription } from '../Subscription';\nimport { from } from './from';\nexport function zip(...sources) {\n let resultSelector = undefined;\n if (typeof sources[sources.length - 1] === 'function') {\n resultSelector = sources.pop();\n }\n return new Observable((subscriber) => {\n const buffers = sources.map(() => []);\n const completed = sources.map(() => false);\n const subscription = new Subscription();\n const tryEmit = () => {\n if (buffers.every((buffer) => buffer.length > 0)) {\n let result = buffers.map((buffer) => buffer.shift());\n if (resultSelector) {\n try {\n result = resultSelector(...result);\n }\n catch (err) {\n subscriber.error(err);\n return;\n }\n }\n subscriber.next(result);\n if (buffers.some((buffer, i) => buffer.length === 0 && completed[i])) {\n subscriber.complete();\n }\n }\n };\n for (let i = 0; !subscriber.closed && i < sources.length; i++) {\n const source = from(sources[i]);\n subscription.add(source.subscribe({\n next: (value) => {\n buffers[i].push(value);\n tryEmit();\n },\n error: (err) => subscriber.error(err),\n complete: () => {\n completed[i] = true;\n if (buffers[i].length === 0) {\n subscriber.complete();\n }\n },\n }));\n }\n return subscription;\n });\n}\n//# sourceMappingURL=zip.js.map","const { isArray } = Array;\nexport function argsOrArgArray(args) {\n return args.length === 1 && isArray(args[0]) ? args[0] : args;\n}\n//# sourceMappingURL=argsOrArgArray.js.map","import { isScheduler } from '../util/isScheduler';\nimport { mergeAll } from '../operators/mergeAll';\nimport { fromArray } from './fromArray';\nimport { argsOrArgArray } from '../util/argsOrArgArray';\nimport { from } from './from';\nimport { EMPTY } from './empty';\nexport function merge(...args) {\n let concurrent = Infinity;\n let scheduler = undefined;\n if (isScheduler(args[args.length - 1])) {\n scheduler = args.pop();\n }\n if (typeof args[args.length - 1] === 'number') {\n concurrent = args.pop();\n }\n args = argsOrArgArray(args);\n return !args.length\n ?\n EMPTY\n : args.length === 1\n ?\n from(args[0])\n :\n mergeAll(concurrent)(fromArray(args, scheduler));\n}\n//# sourceMappingURL=merge.js.map","import { asyncScheduler } from '../scheduler/async';\nimport { isValidDate } from '../util/isDate';\nimport { operate } from '../util/lift';\nimport { OperatorSubscriber } from './OperatorSubscriber';\nexport function delay(delay, scheduler = asyncScheduler) {\n return operate((source, subscriber) => {\n const isAbsoluteDelay = isValidDate(delay);\n let isComplete = false;\n let active = 0;\n let absoluteTimeValues = isAbsoluteDelay ? [] : null;\n const checkComplete = () => isComplete && !active && !(absoluteTimeValues === null || absoluteTimeValues === void 0 ? void 0 : absoluteTimeValues.length) && subscriber.complete();\n if (isAbsoluteDelay) {\n active++;\n subscriber.add(scheduler.schedule(() => {\n active--;\n if (absoluteTimeValues) {\n const values = absoluteTimeValues;\n absoluteTimeValues = null;\n for (const value of values) {\n subscriber.next(value);\n }\n }\n checkComplete();\n }, +delay - scheduler.now()));\n }\n source.subscribe(new OperatorSubscriber(subscriber, (value) => {\n if (isAbsoluteDelay) {\n absoluteTimeValues ? absoluteTimeValues.push(value) : subscriber.next(value);\n }\n else {\n active++;\n subscriber.add(scheduler.schedule(() => {\n active--;\n subscriber.next(value);\n checkComplete();\n }, delay));\n }\n }, undefined, () => {\n isComplete = true;\n checkComplete();\n }));\n return () => {\n absoluteTimeValues = null;\n };\n });\n}\n//# sourceMappingURL=delay.js.map","export function isValidDate(value) {\n return value instanceof Date && !isNaN(value);\n}\n//# sourceMappingURL=isDate.js.map"],"sourceRoot":""} \ No newline at end of file diff --git a/v2/assets/javascripts/worker/search.4ac00218.min.js b/v2/assets/javascripts/worker/search.4ac00218.min.js new file mode 100644 index 000000000..0db84684e --- /dev/null +++ b/v2/assets/javascripts/worker/search.4ac00218.min.js @@ -0,0 +1,59 @@ +!function(e){var t={};function r(n){if(t[n])return t[n].exports;var i=t[n]={i:n,l:!1,exports:{}};return e[n].call(i.exports,i,i.exports,r),i.l=!0,i.exports}r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)r.d(n,i,function(t){return e[t]}.bind(null,i));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=5)}([function(e,t,r){"use strict"; +/*! + * escape-html + * Copyright(c) 2012-2013 TJ Holowaychuk + * Copyright(c) 2015 Andreas Lubbe + * Copyright(c) 2015 Tiancheng "Timothy" Gu + * MIT Licensed + */var n=/["'&<>]/;e.exports=function(e){var t,r=""+e,i=n.exec(r);if(!i)return r;var s="",o=0,a=0;for(o=i.index;o0){var u=I.utils.clone(t)||{};u.position=[o,a],u.index=i.length,i.push(new I.Token(r.slice(o,s),u))}o=s+1}}return i},I.tokenizer.separator=/[\s\-]+/ +/*! + * lunr.Pipeline + * Copyright (C) 2020 Oliver Nightingale + */,I.Pipeline=function(){this._stack=[]},I.Pipeline.registeredFunctions=Object.create(null),I.Pipeline.registerFunction=function(e,t){t in this.registeredFunctions&&I.utils.warn("Overwriting existing registered function: "+t),e.label=t,I.Pipeline.registeredFunctions[e.label]=e},I.Pipeline.warnIfFunctionNotRegistered=function(e){e.label&&e.label in this.registeredFunctions||I.utils.warn("Function is not registered with pipeline. This may cause problems when serialising the index.\n",e)},I.Pipeline.load=function(e){var t=new I.Pipeline;return e.forEach((function(e){var r=I.Pipeline.registeredFunctions[e];if(!r)throw new Error("Cannot load unregistered function: "+e);t.add(r)})),t},I.Pipeline.prototype.add=function(){var e=Array.prototype.slice.call(arguments);e.forEach((function(e){I.Pipeline.warnIfFunctionNotRegistered(e),this._stack.push(e)}),this)},I.Pipeline.prototype.after=function(e,t){I.Pipeline.warnIfFunctionNotRegistered(t);var r=this._stack.indexOf(e);if(-1==r)throw new Error("Cannot find existingFn");r+=1,this._stack.splice(r,0,t)},I.Pipeline.prototype.before=function(e,t){I.Pipeline.warnIfFunctionNotRegistered(t);var r=this._stack.indexOf(e);if(-1==r)throw new Error("Cannot find existingFn");this._stack.splice(r,0,t)},I.Pipeline.prototype.remove=function(e){var t=this._stack.indexOf(e);-1!=t&&this._stack.splice(t,1)},I.Pipeline.prototype.run=function(e){for(var t=this._stack.length,r=0;r1&&(se&&(r=i),s!=e);)n=r-t,i=t+Math.floor(n/2),s=this.elements[2*i];return s==e||s>e?2*i:sa?l+=2:o==a&&(t+=r[u+1]*n[l+1],u+=2,l+=2);return t},I.Vector.prototype.similarity=function(e){return this.dot(e)/this.magnitude()||0},I.Vector.prototype.toArray=function(){for(var e=new Array(this.elements.length/2),t=1,r=0;t0){var s,o=i.str.charAt(0);o in i.node.edges?s=i.node.edges[o]:(s=new I.TokenSet,i.node.edges[o]=s),1==i.str.length&&(s.final=!0),n.push({node:s,editsRemaining:i.editsRemaining,str:i.str.slice(1)})}if(0!=i.editsRemaining){if("*"in i.node.edges)var a=i.node.edges["*"];else{a=new I.TokenSet;i.node.edges["*"]=a}if(0==i.str.length&&(a.final=!0),n.push({node:a,editsRemaining:i.editsRemaining-1,str:i.str}),i.str.length>1&&n.push({node:i.node,editsRemaining:i.editsRemaining-1,str:i.str.slice(1)}),1==i.str.length&&(i.node.final=!0),i.str.length>=1){if("*"in i.node.edges)var u=i.node.edges["*"];else{u=new I.TokenSet;i.node.edges["*"]=u}1==i.str.length&&(u.final=!0),n.push({node:u,editsRemaining:i.editsRemaining-1,str:i.str.slice(1)})}if(i.str.length>1){var l,c=i.str.charAt(0),h=i.str.charAt(1);h in i.node.edges?l=i.node.edges[h]:(l=new I.TokenSet,i.node.edges[h]=l),1==i.str.length&&(l.final=!0),n.push({node:l,editsRemaining:i.editsRemaining-1,str:c+i.str.slice(2)})}}}return r},I.TokenSet.fromString=function(e){for(var t=new I.TokenSet,r=t,n=0,i=e.length;n=e;t--){var r=this.uncheckedNodes[t],n=r.child.toString();n in this.minimizedNodes?r.parent.edges[r.char]=this.minimizedNodes[n]:(r.child._str=n,this.minimizedNodes[n]=r.child),this.uncheckedNodes.pop()}} +/*! + * lunr.Index + * Copyright (C) 2020 Oliver Nightingale + */,I.Index=function(e){this.invertedIndex=e.invertedIndex,this.fieldVectors=e.fieldVectors,this.tokenSet=e.tokenSet,this.fields=e.fields,this.pipeline=e.pipeline},I.Index.prototype.search=function(e){return this.query((function(t){new I.QueryParser(e,t).parse()}))},I.Index.prototype.query=function(e){for(var t=new I.Query(this.fields),r=Object.create(null),n=Object.create(null),i=Object.create(null),s=Object.create(null),o=Object.create(null),a=0;a1?1:e},I.Builder.prototype.k1=function(e){this._k1=e},I.Builder.prototype.add=function(e,t){var r=e[this._ref],n=Object.keys(this._fields);this._documents[r]=t||{},this.documentCount+=1;for(var i=0;i=this.length)return I.QueryLexer.EOS;var e=this.str.charAt(this.pos);return this.pos+=1,e},I.QueryLexer.prototype.width=function(){return this.pos-this.start},I.QueryLexer.prototype.ignore=function(){this.start==this.pos&&(this.pos+=1),this.start=this.pos},I.QueryLexer.prototype.backup=function(){this.pos-=1},I.QueryLexer.prototype.acceptDigitRun=function(){var e,t;do{t=(e=this.next()).charCodeAt(0)}while(t>47&&t<58);e!=I.QueryLexer.EOS&&this.backup()},I.QueryLexer.prototype.more=function(){return this.pos1&&(e.backup(),e.emit(I.QueryLexer.TERM)),e.ignore(),e.more())return I.QueryLexer.lexText},I.QueryLexer.lexEditDistance=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(I.QueryLexer.EDIT_DISTANCE),I.QueryLexer.lexText},I.QueryLexer.lexBoost=function(e){return e.ignore(),e.acceptDigitRun(),e.emit(I.QueryLexer.BOOST),I.QueryLexer.lexText},I.QueryLexer.lexEOS=function(e){e.width()>0&&e.emit(I.QueryLexer.TERM)},I.QueryLexer.termSeparator=I.tokenizer.separator,I.QueryLexer.lexText=function(e){for(;;){var t=e.next();if(t==I.QueryLexer.EOS)return I.QueryLexer.lexEOS;if(92!=t.charCodeAt(0)){if(":"==t)return I.QueryLexer.lexField;if("~"==t)return e.backup(),e.width()>0&&e.emit(I.QueryLexer.TERM),I.QueryLexer.lexEditDistance;if("^"==t)return e.backup(),e.width()>0&&e.emit(I.QueryLexer.TERM),I.QueryLexer.lexBoost;if("+"==t&&1===e.width())return e.emit(I.QueryLexer.PRESENCE),I.QueryLexer.lexText;if("-"==t&&1===e.width())return e.emit(I.QueryLexer.PRESENCE),I.QueryLexer.lexText;if(t.match(I.QueryLexer.termSeparator))return I.QueryLexer.lexTerm}else e.escapeCharacter()}},I.QueryParser=function(e,t){this.lexer=new I.QueryLexer(e),this.query=t,this.currentClause={},this.lexemeIdx=0},I.QueryParser.prototype.parse=function(){this.lexer.run(),this.lexemes=this.lexer.lexemes;for(var e=I.QueryParser.parseClause;e;)e=e(this);return this.query},I.QueryParser.prototype.peekLexeme=function(){return this.lexemes[this.lexemeIdx]},I.QueryParser.prototype.consumeLexeme=function(){var e=this.peekLexeme();return this.lexemeIdx+=1,e},I.QueryParser.prototype.nextClause=function(){var e=this.currentClause;this.query.clause(e),this.currentClause={}},I.QueryParser.parseClause=function(e){var t=e.peekLexeme();if(null!=t)switch(t.type){case I.QueryLexer.PRESENCE:return I.QueryParser.parsePresence;case I.QueryLexer.FIELD:return I.QueryParser.parseField;case I.QueryLexer.TERM:return I.QueryParser.parseTerm;default:var r="expected either a field or a term, found "+t.type;throw t.str.length>=1&&(r+=" with value '"+t.str+"'"),new I.QueryParseError(r,t.start,t.end)}},I.QueryParser.parsePresence=function(e){var t=e.consumeLexeme();if(null!=t){switch(t.str){case"-":e.currentClause.presence=I.Query.presence.PROHIBITED;break;case"+":e.currentClause.presence=I.Query.presence.REQUIRED;break;default:var r="unrecognised presence operator'"+t.str+"'";throw new I.QueryParseError(r,t.start,t.end)}var n=e.peekLexeme();if(null==n){r="expecting term or field, found nothing";throw new I.QueryParseError(r,t.start,t.end)}switch(n.type){case I.QueryLexer.FIELD:return I.QueryParser.parseField;case I.QueryLexer.TERM:return I.QueryParser.parseTerm;default:r="expecting term or field, found '"+n.type+"'";throw new I.QueryParseError(r,n.start,n.end)}}},I.QueryParser.parseField=function(e){var t=e.consumeLexeme();if(null!=t){if(-1==e.query.allFields.indexOf(t.str)){var r=e.query.allFields.map((function(e){return"'"+e+"'"})).join(", "),n="unrecognised field '"+t.str+"', possible fields: "+r;throw new I.QueryParseError(n,t.start,t.end)}e.currentClause.fields=[t.str];var i=e.peekLexeme();if(null==i){n="expecting term, found nothing";throw new I.QueryParseError(n,t.start,t.end)}switch(i.type){case I.QueryLexer.TERM:return I.QueryParser.parseTerm;default:n="expecting term, found '"+i.type+"'";throw new I.QueryParseError(n,i.start,i.end)}}},I.QueryParser.parseTerm=function(e){var t=e.consumeLexeme();if(null!=t){e.currentClause.term=t.str.toLowerCase(),-1!=t.str.indexOf("*")&&(e.currentClause.usePipeline=!1);var r=e.peekLexeme();if(null!=r)switch(r.type){case I.QueryLexer.TERM:return e.nextClause(),I.QueryParser.parseTerm;case I.QueryLexer.FIELD:return e.nextClause(),I.QueryParser.parseField;case I.QueryLexer.EDIT_DISTANCE:return I.QueryParser.parseEditDistance;case I.QueryLexer.BOOST:return I.QueryParser.parseBoost;case I.QueryLexer.PRESENCE:return e.nextClause(),I.QueryParser.parsePresence;default:var n="Unexpected lexeme type '"+r.type+"'";throw new I.QueryParseError(n,r.start,r.end)}else e.nextClause()}},I.QueryParser.parseEditDistance=function(e){var t=e.consumeLexeme();if(null!=t){var r=parseInt(t.str,10);if(isNaN(r)){var n="edit distance must be numeric";throw new I.QueryParseError(n,t.start,t.end)}e.currentClause.editDistance=r;var i=e.peekLexeme();if(null!=i)switch(i.type){case I.QueryLexer.TERM:return e.nextClause(),I.QueryParser.parseTerm;case I.QueryLexer.FIELD:return e.nextClause(),I.QueryParser.parseField;case I.QueryLexer.EDIT_DISTANCE:return I.QueryParser.parseEditDistance;case I.QueryLexer.BOOST:return I.QueryParser.parseBoost;case I.QueryLexer.PRESENCE:return e.nextClause(),I.QueryParser.parsePresence;default:n="Unexpected lexeme type '"+i.type+"'";throw new I.QueryParseError(n,i.start,i.end)}else e.nextClause()}},I.QueryParser.parseBoost=function(e){var t=e.consumeLexeme();if(null!=t){var r=parseInt(t.str,10);if(isNaN(r)){var n="boost must be numeric";throw new I.QueryParseError(n,t.start,t.end)}e.currentClause.boost=r;var i=e.peekLexeme();if(null!=i)switch(i.type){case I.QueryLexer.TERM:return e.nextClause(),I.QueryParser.parseTerm;case I.QueryLexer.FIELD:return e.nextClause(),I.QueryParser.parseField;case I.QueryLexer.EDIT_DISTANCE:return I.QueryParser.parseEditDistance;case I.QueryLexer.BOOST:return I.QueryParser.parseBoost;case I.QueryLexer.PRESENCE:return e.nextClause(),I.QueryParser.parsePresence;default:n="Unexpected lexeme type '"+i.type+"'";throw new I.QueryParseError(n,i.start,i.end)}else e.nextClause()}},void 0===(i="function"==typeof(n=function(){return I})?n.call(t,r,t,e):n)||(e.exports=i)}()},function(e,t,r){"use strict";(function(t){e.exports=function(){if("object"==typeof globalThis)return globalThis;var e;try{e=this||new Function("return this")()}catch(e){if("object"==typeof window)return window;if("object"==typeof self)return self;if(void 0!==t)return t}return e}()}).call(this,r(4))},function(e,t){var r;r=function(){return this}();try{r=r||new Function("return this")()}catch(e){"object"==typeof window&&(r=window)}e.exports=r},function(e,t,r){"use strict";r.r(t),r.d(t,"handler",(function(){return u}));function n(e,t,r,n){return new(r||(r=Promise))((function(i,s){function o(e){try{u(n.next(e))}catch(e){s(e)}}function a(e){try{u(n.throw(e))}catch(e){s(e)}}function u(e){var t;e.done?i(e.value):(t=e.value,t instanceof r?t:new r((function(e){e(t)}))).then(o,a)}u((n=n.apply(e,t||[])).next())}))}Object.create;Object.create;r(1);var i,s=r(0);class o{constructor({config:e,docs:t,pipeline:r,index:n}){this.documents=function(e){const t=new Map,r=new Set;for(const n of e){const[e,i]=n.location.split("#"),o=n.location,a=n.title,u=s(n.text).replace(/\s+(?=[,.:;!?])/g,"").replace(/\s+/g," ");if(i){const i=t.get(e);r.has(i)?t.set(o,{location:o,title:a,text:u,parent:i}):(i.title=n.title,i.text=u,r.add(i))}else t.set(o,{location:o,title:a,text:u})}return t}(t),this.highlight=function(e){const t=new RegExp(e.separator,"img"),r=(e,t,r)=>`${t}${r}`;return n=>{n=n.replace(/[\s*+\-:~^]+/g," ").trim();const i=new RegExp(`(^|${e.separator})(${n.replace(/[|\\{}()[\]^$+*?.-]/g,"\\$&").replace(t,"|")})`,"img");return e=>e.replace(i,r).replace(/<\/mark>(\s+)]*>/gim,"$1")}}(e),lunr.tokenizer.separator=new RegExp(e.separator),this.index=void 0===n?lunr((function(){1===e.lang.length&&"en"!==e.lang[0]?this.use(lunr[e.lang[0]]):e.lang.length>1&&this.use(lunr.multiLanguage(...e.lang));const n=function(e,t){const[r,n]=[new Set(e),new Set(t)];return[...new Set([...r].filter(e=>!n.has(e)))]}(["trimmer","stopWordFilter","stemmer"],r);for(const t of e.lang.map(e=>"en"===e?lunr:lunr[e]))for(const e of n)this.pipeline.remove(t[e]),this.searchPipeline.remove(t[e]);this.field("title",{boost:1e3}),this.field("text"),this.ref("location");for(const e of t)this.add(e)})):lunr.Index.load(n)}search(e){if(e)try{const t=this.highlight(e),r=function(e){const t=new lunr.Query(["title","text"]);return new lunr.QueryParser(e,t).parse(),t.clauses}(e).filter(e=>e.presence!==lunr.Query.presence.PROHIBITED);return[...this.index.search(e+"*").reduce((e,{ref:n,score:i,matchData:s})=>{const o=this.documents.get(n);if(void 0!==o){const{location:n,title:a,text:u,parent:l}=o,c=function(e,t){const r=new Set(e),n={};for(let e=0;ee);e.push({location:n,title:t(a),text:t(u),score:i*(1+h),terms:c})}return e},[]).sort((e,t)=>t.score-e.score).reduce((e,t)=>{const r=this.documents.get(t.location);if(void 0!==r){const n="parent"in r?r.parent.location:r.location;e.set(n,[...e.get(n)||[],t])}return e},new Map).values()]}catch(t){console.warn(`Invalid query: ${e} – see https://bit.ly/2s3ChXG`)}return[]}}let a;function u(e){return n(this,void 0,void 0,(function*(){switch(e.type){case i.SETUP:return yield function(e){return n(this,void 0,void 0,(function*(){let t="../lunr";if("undefined"!=typeof parent&&"IFrameWorker"in parent){const e=document.querySelector("script[src]"),[r]=e.src.split("/worker");t=t.replace("..",r)}const r=[];for(const n of e.lang)"ja"===n&&r.push(t+"/tinyseg.min.js"),"en"!==n&&r.push(`${t}/min/lunr.${n}.min.js`);e.lang.length>1&&r.push(t+"/min/lunr.multi.min.js"),r.length&&(yield importScripts(t+"/min/lunr.stemmer.support.min.js",...r))}))}(e.data.config),a=new o(e.data),{type:i.READY};case i.QUERY:return{type:i.RESULT,data:a?a.search(e.data):[]};default:throw new TypeError("Invalid message type")}}))}!function(e){e[e.SETUP=0]="SETUP",e[e.READY=1]="READY",e[e.QUERY=2]="QUERY",e[e.RESULT=3]="RESULT"}(i||(i={})),addEventListener("message",e=>n(void 0,void 0,void 0,(function*(){postMessage(yield u(e.data))})))}]); +//# sourceMappingURL=search.4ac00218.min.js.map \ No newline at end of file diff --git a/v2/assets/javascripts/worker/search.4ac00218.min.js.map b/v2/assets/javascripts/worker/search.4ac00218.min.js.map new file mode 100644 index 000000000..f2a013179 --- /dev/null +++ b/v2/assets/javascripts/worker/search.4ac00218.min.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///./node_modules/escape-html/index.js","webpack:///./node_modules/lunr/lunr-exposed.js","webpack:///./node_modules/lunr/lunr.js","webpack:///./node_modules/expose-loader/dist/runtime/getGlobalThis.js","webpack:///(webpack)/buildin/global.js","webpack:///./node_modules/tslib/tslib.es6.js","webpack:///./src/assets/javascripts/integrations/search/worker/message/index.ts","webpack:///./src/assets/javascripts/integrations/search/_/index.ts","webpack:///./src/assets/javascripts/integrations/search/document/index.ts","webpack:///./src/assets/javascripts/integrations/search/highlighter/index.ts","webpack:///./src/assets/javascripts/integrations/search/query/_/index.ts","webpack:///./src/assets/javascripts/integrations/search/worker/main/index.ts"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s","matchHtmlRegExp","string","escape","str","match","exec","html","index","lastIndex","length","charCodeAt","substring","___EXPOSE_LOADER_IMPORT___","___EXPOSE_LOADER_GLOBAL_THIS___","global","step2list","step3list","v","C","re_mgr0","re_mgr1","re_meq1","re_s_v","re_1a","re2_1a","re_1b","re2_1b","re_1b_2","re2_1b_2","re3_1b_2","re4_1b_2","re_1c","re_2","re_3","re_4","re2_4","re_5","re_5_1","re3_5","porterStemmer","lunr","config","builder","Builder","pipeline","add","trimmer","stopWordFilter","stemmer","searchPipeline","build","version","utils","warn","this","message","console","asString","obj","toString","clone","keys","val","Array","isArray","slice","TypeError","FieldRef","docRef","fieldName","stringValue","_stringValue","joiner","fromString","indexOf","fieldRef","undefined","Set","elements","complete","intersect","other","union","contains","empty","a","b","intersection","element","push","concat","idf","posting","documentCount","documentsWithTerm","x","Math","log","abs","Token","metadata","update","fn","tokenizer","map","toLowerCase","len","tokens","sliceEnd","sliceStart","sliceLength","charAt","separator","tokenMetadata","Pipeline","_stack","registeredFunctions","registerFunction","label","warnIfFunctionNotRegistered","load","serialised","forEach","fnName","Error","fns","arguments","after","existingFn","newFn","pos","splice","before","remove","run","stackLength","memo","j","result","k","runString","token","reset","toJSON","Vector","_magnitude","positionForIndex","start","end","pivotPoint","floor","pivotIndex","insert","insertIdx","upsert","position","magnitude","sumOfSquares","elementsLength","sqrt","dot","otherVector","dotProduct","aLen","bLen","aVal","bVal","similarity","toArray","output","RegExp","w","stem","suffix","firstch","re","re2","re3","re4","substr","toUpperCase","test","replace","fp","generateStopWordFilter","stopWords","words","reduce","stopWord","TokenSet","final","edges","id","_nextId","fromArray","arr","finish","root","fromClause","clause","fromFuzzyString","term","editDistance","stack","node","editsRemaining","frame","pop","noEditNode","char","insertionNode","substitutionNode","transposeNode","charA","charB","next","prefix","edge","_str","labels","sort","qNode","qEdges","qLen","nEdges","nLen","q","qEdge","nEdge","previousWord","uncheckedNodes","minimizedNodes","word","commonPrefix","minimize","child","nextNode","parent","downTo","childKey","Index","attrs","invertedIndex","fieldVectors","tokenSet","fields","search","queryString","query","QueryParser","parse","Query","matchingFields","queryVectors","termFieldCache","requiredMatches","prohibitedMatches","clauses","terms","clauseMatches","usePipeline","termTokenSet","expandedTerms","presence","REQUIRED","field","expandedTerm","termIndex","_index","fieldPosting","matchingDocumentRefs","termField","matchingDocumentsSet","PROHIBITED","boost","fieldMatch","matchingDocumentRef","matchingFieldRef","MatchData","allRequiredMatches","allProhibitedMatches","matchingFieldRefs","results","matches","isNegated","docMatch","fieldVector","score","matchData","combine","ref","serializedIndex","serializedVectors","serializedInvertedIndex","tokenSetBuilder","tuple","_ref","_fields","_documents","fieldTermFrequencies","fieldLengths","_b","_k1","metadataWhitelist","attributes","RangeError","number","k1","doc","extractor","fieldTerms","metadataKey","calculateAverageFieldLengths","fieldRefs","numberOfFields","accumulator","documentsWithField","averageFieldLength","createFieldVectors","fieldRefsLength","termIdfCache","fieldLength","termFrequencies","termsLength","fieldBoost","docBoost","scoreWithPrecision","tf","round","createTokenSet","use","args","unshift","apply","clonedMetadata","metadataKeys","otherMatchData","allFields","wildcard","String","NONE","LEADING","TRAILING","OPTIONAL","options","QueryParseError","QueryLexer","lexemes","escapeCharPositions","state","lexText","sliceString","subSlices","join","emit","type","escapeCharacter","EOS","width","ignore","backup","acceptDigitRun","charCode","more","FIELD","TERM","EDIT_DISTANCE","BOOST","PRESENCE","lexField","lexer","lexTerm","lexEditDistance","lexBoost","lexEOS","termSeparator","currentClause","lexemeIdx","parseClause","peekLexeme","consumeLexeme","lexeme","nextClause","completedClause","parser","parsePresence","parseField","parseTerm","errorMessage","nextLexeme","possibleFields","f","parseEditDistance","parseBoost","parseInt","isNaN","globalThis","g","Function","e","window","self","__awaiter","thisArg","_arguments","P","generator","Promise","resolve","reject","fulfilled","step","rejected","done","then","SearchMessageType","docs","documents","Map","parents","path","hash","location","split","title","text","has","set","setupSearchDocumentMap","highlight","_","data","trim","setupSearchHighlighter","lang","multiLanguage","y","filter","difference","language","parseSearchQuery","document","startsWith","delete","getSearchQueryTerms","values","every","handler","SETUP","base","worker","querySelector","src","scripts","importScripts","setupSearchLanguages","READY","QUERY","RESULT","addEventListener","ev","postMessage"],"mappings":"aACE,IAAIA,EAAmB,GAGvB,SAASC,EAAoBC,GAG5B,GAAGF,EAAiBE,GACnB,OAAOF,EAAiBE,GAAUC,QAGnC,IAAIC,EAASJ,EAAiBE,GAAY,CACzCG,EAAGH,EACHI,GAAG,EACHH,QAAS,IAUV,OANAI,EAAQL,GAAUM,KAAKJ,EAAOD,QAASC,EAAQA,EAAOD,QAASF,GAG/DG,EAAOE,GAAI,EAGJF,EAAOD,QAKfF,EAAoBQ,EAAIF,EAGxBN,EAAoBS,EAAIV,EAGxBC,EAAoBU,EAAI,SAASR,EAASS,EAAMC,GAC3CZ,EAAoBa,EAAEX,EAASS,IAClCG,OAAOC,eAAeb,EAASS,EAAM,CAAEK,YAAY,EAAMC,IAAKL,KAKhEZ,EAAoBkB,EAAI,SAAShB,GACX,oBAAXiB,QAA0BA,OAAOC,aAC1CN,OAAOC,eAAeb,EAASiB,OAAOC,YAAa,CAAEC,MAAO,WAE7DP,OAAOC,eAAeb,EAAS,aAAc,CAAEmB,OAAO,KAQvDrB,EAAoBsB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQrB,EAAoBqB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,iBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKX,OAAOY,OAAO,MAGvB,GAFA1B,EAAoBkB,EAAEO,GACtBX,OAAOC,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOrB,EAAoBU,EAAEe,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRzB,EAAoB6B,EAAI,SAAS1B,GAChC,IAAIS,EAAST,GAAUA,EAAOqB,WAC7B,WAAwB,OAAOrB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAH,EAAoBU,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRZ,EAAoBa,EAAI,SAASiB,EAAQC,GAAY,OAAOjB,OAAOkB,UAAUC,eAAe1B,KAAKuB,EAAQC,IAGzG/B,EAAoBkC,EAAI,GAIjBlC,EAAoBA,EAAoBmC,EAAI,G;;;;;;;GCnErD,IAAIC,EAAkB,UAOtBjC,EAAOD,QAUP,SAAoBmC,GAClB,IAOIC,EAPAC,EAAM,GAAKF,EACXG,EAAQJ,EAAgBK,KAAKF,GAEjC,IAAKC,EACH,OAAOD,EAIT,IAAIG,EAAO,GACPC,EAAQ,EACRC,EAAY,EAEhB,IAAKD,EAAQH,EAAMG,MAAOA,EAAQJ,EAAIM,OAAQF,IAAS,CACrD,OAAQJ,EAAIO,WAAWH,IACrB,KAAK,GACHL,EAAS,SACT,MACF,KAAK,GACHA,EAAS,QACT,MACF,KAAK,GACHA,EAAS,QACT,MACF,KAAK,GACHA,EAAS,OACT,MACF,KAAK,GACHA,EAAS,OACT,MACF,QACE,SAGAM,IAAcD,IAChBD,GAAQH,EAAIQ,UAAUH,EAAWD,IAGnCC,EAAYD,EAAQ,EACpBD,GAAQJ,EAGV,OAAOM,IAAcD,EACjBD,EAAOH,EAAIQ,UAAUH,EAAWD,GAChCD,I,gBC5EN,IAAIM,EAA6B,EAAQ,GAErCC,EADsC,EAAQ,QAEK,IAA5CA,EAAsC,OAAmBA,EAAsC,KAAID,GAC9G7C,EAAOD,QAAU8C,G,gBCJjB;;;;;IAMC,WAiCD,IAoC6BE,EAw2BvBC,EAwBFC,EAWAC,EACAC,EAQEC,EACAC,EACAC,EACAC,EAEAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAEAC,EACAC,EAEAC,EAEAC,EACAC,EAEAC,EACAC,EACAC,EAEAC,EAl9BFC,EAAO,SAAUC,GACnB,IAAIC,EAAU,IAAIF,EAAKG,QAavB,OAXAD,EAAQE,SAASC,IACfL,EAAKM,QACLN,EAAKO,eACLP,EAAKQ,SAGPN,EAAQO,eAAeJ,IACrBL,EAAKQ,SAGPP,EAAOtE,KAAKuE,EAASA,GACdA,EAAQQ,SAGjBV,EAAKW,QAAU;;;;IAUfX,EAAKY,MAAQ,GASbZ,EAAKY,MAAMC,MAAkBvC,EAQ1BwC,KANM,SAAUC,GACXzC,EAAO0C,SAAWA,QAAQH,MAC5BG,QAAQH,KAAKE,KAiBnBf,EAAKY,MAAMK,SAAW,SAAUC,GAC9B,OAAIA,QACK,GAEAA,EAAIC,YAoBfnB,EAAKY,MAAMQ,MAAQ,SAAUF,GAC3B,GAAIA,QACF,OAAOA,EAMT,IAHA,IAAIE,EAAQlF,OAAOY,OAAO,MACtBuE,EAAOnF,OAAOmF,KAAKH,GAEd1F,EAAI,EAAGA,EAAI6F,EAAKpD,OAAQzC,IAAK,CACpC,IAAIuB,EAAMsE,EAAK7F,GACX8F,EAAMJ,EAAInE,GAEd,GAAIwE,MAAMC,QAAQF,GAChBF,EAAMrE,GAAOuE,EAAIG,YADnB,CAKA,GAAmB,iBAARH,GACQ,iBAARA,GACQ,kBAARA,EAKX,MAAM,IAAII,UAAU,yDAJlBN,EAAMrE,GAAOuE,GAOjB,OAAOF,GAETpB,EAAK2B,SAAW,SAAUC,EAAQC,EAAWC,GAC3ChB,KAAKc,OAASA,EACdd,KAAKe,UAAYA,EACjBf,KAAKiB,aAAeD,GAGtB9B,EAAK2B,SAASK,OAAS,IAEvBhC,EAAK2B,SAASM,WAAa,SAAU1E,GACnC,IAAIN,EAAIM,EAAE2E,QAAQlC,EAAK2B,SAASK,QAEhC,IAAW,IAAP/E,EACF,KAAM,6BAGR,IAAIkF,EAAW5E,EAAEkE,MAAM,EAAGxE,GACtB2E,EAASrE,EAAEkE,MAAMxE,EAAI,GAEzB,OAAO,IAAI+C,EAAK2B,SAAUC,EAAQO,EAAU5E,IAG9CyC,EAAK2B,SAASvE,UAAU+D,SAAW,WAKjC,OAJyBiB,MAArBtB,KAAKiB,eACPjB,KAAKiB,aAAejB,KAAKe,UAAY7B,EAAK2B,SAASK,OAASlB,KAAKc,QAG5Dd,KAAKiB;;;;IAYd/B,EAAKqC,IAAM,SAAUC,GAGnB,GAFAxB,KAAKwB,SAAWpG,OAAOY,OAAO,MAE1BwF,EAAU,CACZxB,KAAK7C,OAASqE,EAASrE,OAEvB,IAAK,IAAIzC,EAAI,EAAGA,EAAIsF,KAAK7C,OAAQzC,IAC/BsF,KAAKwB,SAASA,EAAS9G,KAAM,OAG/BsF,KAAK7C,OAAS,GAWlB+B,EAAKqC,IAAIE,SAAW,CAClBC,UAAW,SAAUC,GACnB,OAAOA,GAGTC,MAAO,WACL,OAAO5B,MAGT6B,SAAU,WACR,OAAO,IAWX3C,EAAKqC,IAAIO,MAAQ,CACfJ,UAAW,WACT,OAAO1B,MAGT4B,MAAO,SAAUD,GACf,OAAOA,GAGTE,SAAU,WACR,OAAO,IAUX3C,EAAKqC,IAAIjF,UAAUuF,SAAW,SAAUzF,GACtC,QAAS4D,KAAKwB,SAASpF,IAWzB8C,EAAKqC,IAAIjF,UAAUoF,UAAY,SAAUC,GACvC,IAAII,EAAGC,EAAGR,EAAUS,EAAe,GAEnC,GAAIN,IAAUzC,EAAKqC,IAAIE,SACrB,OAAOzB,KAGT,GAAI2B,IAAUzC,EAAKqC,IAAIO,MACrB,OAAOH,EAGL3B,KAAK7C,OAASwE,EAAMxE,QACtB4E,EAAI/B,KACJgC,EAAIL,IAEJI,EAAIJ,EACJK,EAAIhC,MAGNwB,EAAWpG,OAAOmF,KAAKwB,EAAEP,UAEzB,IAAK,IAAI9G,EAAI,EAAGA,EAAI8G,EAASrE,OAAQzC,IAAK,CACxC,IAAIwH,EAAUV,EAAS9G,GACnBwH,KAAWF,EAAER,UACfS,EAAaE,KAAKD,GAItB,OAAO,IAAIhD,EAAKqC,IAAKU,IAUvB/C,EAAKqC,IAAIjF,UAAUsF,MAAQ,SAAUD,GACnC,OAAIA,IAAUzC,EAAKqC,IAAIE,SACdvC,EAAKqC,IAAIE,SAGdE,IAAUzC,EAAKqC,IAAIO,MACd9B,KAGF,IAAId,EAAKqC,IAAInG,OAAOmF,KAAKP,KAAKwB,UAAUY,OAAOhH,OAAOmF,KAAKoB,EAAMH,aAU1EtC,EAAKmD,IAAM,SAAUC,EAASC,GAC5B,IAAIC,EAAoB,EAExB,IAAK,IAAIzB,KAAauB,EACH,UAAbvB,IACJyB,GAAqBpH,OAAOmF,KAAK+B,EAAQvB,IAAY5D,QAGvD,IAAIsF,GAAKF,EAAgBC,EAAoB,KAAQA,EAAoB,IAEzE,OAAOE,KAAKC,IAAI,EAAID,KAAKE,IAAIH,KAW/BvD,EAAK2D,MAAQ,SAAUhG,EAAKiG,GAC1B9C,KAAKnD,IAAMA,GAAO,GAClBmD,KAAK8C,SAAWA,GAAY,IAQ9B5D,EAAK2D,MAAMvG,UAAU+D,SAAW,WAC9B,OAAOL,KAAKnD,KAuBdqC,EAAK2D,MAAMvG,UAAUyG,OAAS,SAAUC,GAEtC,OADAhD,KAAKnD,IAAMmG,EAAGhD,KAAKnD,IAAKmD,KAAK8C,UACtB9C,MAUTd,EAAK2D,MAAMvG,UAAUgE,MAAQ,SAAU0C,GAErC,OADAA,EAAKA,GAAM,SAAUvG,GAAK,OAAOA,GAC1B,IAAIyC,EAAK2D,MAAOG,EAAGhD,KAAKnD,IAAKmD,KAAK8C,UAAW9C,KAAK8C;;;;IAyB3D5D,EAAK+D,UAAY,SAAU7C,EAAK0C,GAC9B,GAAW,MAAP1C,GAAsBkB,MAAPlB,EACjB,MAAO,GAGT,GAAIK,MAAMC,QAAQN,GAChB,OAAOA,EAAI8C,KAAI,SAAUtH,GACvB,OAAO,IAAIsD,EAAK2D,MACd3D,EAAKY,MAAMK,SAASvE,GAAGuH,cACvBjE,EAAKY,MAAMQ,MAAMwC,OASvB,IAJA,IAAIjG,EAAMuD,EAAIC,WAAW8C,cACrBC,EAAMvG,EAAIM,OACVkG,EAAS,GAEJC,EAAW,EAAGC,EAAa,EAAGD,GAAYF,EAAKE,IAAY,CAClE,IACIE,EAAcF,EAAWC,EAE7B,GAHW1G,EAAI4G,OAAOH,GAGZxG,MAAMoC,EAAK+D,UAAUS,YAAcJ,GAAYF,EAAM,CAE7D,GAAII,EAAc,EAAG,CACnB,IAAIG,EAAgBzE,EAAKY,MAAMQ,MAAMwC,IAAa,GAClDa,EAAwB,SAAI,CAACJ,EAAYC,GACzCG,EAAqB,MAAIN,EAAOlG,OAEhCkG,EAAOlB,KACL,IAAIjD,EAAK2D,MACPhG,EAAI8D,MAAM4C,EAAYD,GACtBK,IAKNJ,EAAaD,EAAW,GAK5B,OAAOD,GAUTnE,EAAK+D,UAAUS,UAAY;;;;IAmC3BxE,EAAK0E,SAAW,WACd5D,KAAK6D,OAAS,IAGhB3E,EAAK0E,SAASE,oBAAsB1I,OAAOY,OAAO,MAmClDkD,EAAK0E,SAASG,iBAAmB,SAAUf,EAAIgB,GACzCA,KAAShE,KAAK8D,qBAChB5E,EAAKY,MAAMC,KAAK,6CAA+CiE,GAGjEhB,EAAGgB,MAAQA,EACX9E,EAAK0E,SAASE,oBAAoBd,EAAGgB,OAAShB,GAShD9D,EAAK0E,SAASK,4BAA8B,SAAUjB,GACjCA,EAAGgB,OAAUhB,EAAGgB,SAAShE,KAAK8D,qBAG/C5E,EAAKY,MAAMC,KAAK,kGAAmGiD,IAcvH9D,EAAK0E,SAASM,KAAO,SAAUC,GAC7B,IAAI7E,EAAW,IAAIJ,EAAK0E,SAYxB,OAVAO,EAAWC,SAAQ,SAAUC,GAC3B,IAAIrB,EAAK9D,EAAK0E,SAASE,oBAAoBO,GAE3C,IAAIrB,EAGF,MAAM,IAAIsB,MAAM,sCAAwCD,GAFxD/E,EAASC,IAAIyD,MAMV1D,GAUTJ,EAAK0E,SAAStH,UAAUiD,IAAM,WAC5B,IAAIgF,EAAM9D,MAAMnE,UAAUqE,MAAM9F,KAAK2J,WAErCD,EAAIH,SAAQ,SAAUpB,GACpB9D,EAAK0E,SAASK,4BAA4BjB,GAC1ChD,KAAK6D,OAAO1B,KAAKa,KAChBhD,OAYLd,EAAK0E,SAAStH,UAAUmI,MAAQ,SAAUC,EAAYC,GACpDzF,EAAK0E,SAASK,4BAA4BU,GAE1C,IAAIC,EAAM5E,KAAK6D,OAAOzC,QAAQsD,GAC9B,IAAY,GAARE,EACF,MAAM,IAAIN,MAAM,0BAGlBM,GAAY,EACZ5E,KAAK6D,OAAOgB,OAAOD,EAAK,EAAGD,IAY7BzF,EAAK0E,SAAStH,UAAUwI,OAAS,SAAUJ,EAAYC,GACrDzF,EAAK0E,SAASK,4BAA4BU,GAE1C,IAAIC,EAAM5E,KAAK6D,OAAOzC,QAAQsD,GAC9B,IAAY,GAARE,EACF,MAAM,IAAIN,MAAM,0BAGlBtE,KAAK6D,OAAOgB,OAAOD,EAAK,EAAGD,IAQ7BzF,EAAK0E,SAAStH,UAAUyI,OAAS,SAAU/B,GACzC,IAAI4B,EAAM5E,KAAK6D,OAAOzC,QAAQ4B,IAClB,GAAR4B,GAIJ5E,KAAK6D,OAAOgB,OAAOD,EAAK,IAU1B1F,EAAK0E,SAAStH,UAAU0I,IAAM,SAAU3B,GAGtC,IAFA,IAAI4B,EAAcjF,KAAK6D,OAAO1G,OAErBzC,EAAI,EAAGA,EAAIuK,EAAavK,IAAK,CAIpC,IAHA,IAAIsI,EAAKhD,KAAK6D,OAAOnJ,GACjBwK,EAAO,GAEFC,EAAI,EAAGA,EAAI9B,EAAOlG,OAAQgI,IAAK,CACtC,IAAIC,EAASpC,EAAGK,EAAO8B,GAAIA,EAAG9B,GAE9B,GAAI+B,SAAmD,KAAXA,EAE5C,GAAI3E,MAAMC,QAAQ0E,GAChB,IAAK,IAAIC,EAAI,EAAGA,EAAID,EAAOjI,OAAQkI,IACjCH,EAAK/C,KAAKiD,EAAOC,SAGnBH,EAAK/C,KAAKiD,GAId/B,EAAS6B,EAGX,OAAO7B,GAaTnE,EAAK0E,SAAStH,UAAUgJ,UAAY,SAAUzI,EAAKiG,GACjD,IAAIyC,EAAQ,IAAIrG,EAAK2D,MAAOhG,EAAKiG,GAEjC,OAAO9C,KAAKgF,IAAI,CAACO,IAAQrC,KAAI,SAAUtH,GACrC,OAAOA,EAAEyE,eAQbnB,EAAK0E,SAAStH,UAAUkJ,MAAQ,WAC9BxF,KAAK6D,OAAS,IAUhB3E,EAAK0E,SAAStH,UAAUmJ,OAAS,WAC/B,OAAOzF,KAAK6D,OAAOX,KAAI,SAAUF,GAG/B,OAFA9D,EAAK0E,SAASK,4BAA4BjB,GAEnCA,EAAGgB;;;;IAwBd9E,EAAKwG,OAAS,SAAUlE,GACtBxB,KAAK2F,WAAa,EAClB3F,KAAKwB,SAAWA,GAAY,IAc9BtC,EAAKwG,OAAOpJ,UAAUsJ,iBAAmB,SAAU3I,GAEjD,GAA4B,GAAxB+C,KAAKwB,SAASrE,OAChB,OAAO,EAST,IANA,IAAI0I,EAAQ,EACRC,EAAM9F,KAAKwB,SAASrE,OAAS,EAC7BqG,EAAcsC,EAAMD,EACpBE,EAAarD,KAAKsD,MAAMxC,EAAc,GACtCyC,EAAajG,KAAKwB,SAAsB,EAAbuE,GAExBvC,EAAc,IACfyC,EAAahJ,IACf4I,EAAQE,GAGNE,EAAahJ,IACf6I,EAAMC,GAGJE,GAAchJ,IAIlBuG,EAAcsC,EAAMD,EACpBE,EAAaF,EAAQnD,KAAKsD,MAAMxC,EAAc,GAC9CyC,EAAajG,KAAKwB,SAAsB,EAAbuE,GAG7B,OAAIE,GAAchJ,GAIdgJ,EAAahJ,EAHK,EAAb8I,EAOLE,EAAahJ,EACW,GAAlB8I,EAAa,QADvB,GAcF7G,EAAKwG,OAAOpJ,UAAU4J,OAAS,SAAUC,EAAW3F,GAClDR,KAAKoG,OAAOD,EAAW3F,GAAK,WAC1B,KAAM,sBAYVtB,EAAKwG,OAAOpJ,UAAU8J,OAAS,SAAUD,EAAW3F,EAAKwC,GACvDhD,KAAK2F,WAAa,EAClB,IAAIU,EAAWrG,KAAK4F,iBAAiBO,GAEjCnG,KAAKwB,SAAS6E,IAAaF,EAC7BnG,KAAKwB,SAAS6E,EAAW,GAAKrD,EAAGhD,KAAKwB,SAAS6E,EAAW,GAAI7F,GAE9DR,KAAKwB,SAASqD,OAAOwB,EAAU,EAAGF,EAAW3F,IASjDtB,EAAKwG,OAAOpJ,UAAUgK,UAAY,WAChC,GAAItG,KAAK2F,WAAY,OAAO3F,KAAK2F,WAKjC,IAHA,IAAIY,EAAe,EACfC,EAAiBxG,KAAKwB,SAASrE,OAE1BzC,EAAI,EAAGA,EAAI8L,EAAgB9L,GAAK,EAAG,CAC1C,IAAI8F,EAAMR,KAAKwB,SAAS9G,GACxB6L,GAAgB/F,EAAMA,EAGxB,OAAOR,KAAK2F,WAAajD,KAAK+D,KAAKF,IASrCrH,EAAKwG,OAAOpJ,UAAUoK,IAAM,SAAUC,GAOpC,IANA,IAAIC,EAAa,EACb7E,EAAI/B,KAAKwB,SAAUQ,EAAI2E,EAAYnF,SACnCqF,EAAO9E,EAAE5E,OAAQ2J,EAAO9E,EAAE7E,OAC1B4J,EAAO,EAAGC,EAAO,EACjBtM,EAAI,EAAGyK,EAAI,EAERzK,EAAImM,GAAQ1B,EAAI2B,IACrBC,EAAOhF,EAAErH,KAAIsM,EAAOhF,EAAEmD,IAEpBzK,GAAK,EACIqM,EAAOC,EAChB7B,GAAK,EACI4B,GAAQC,IACjBJ,GAAc7E,EAAErH,EAAI,GAAKsH,EAAEmD,EAAI,GAC/BzK,GAAK,EACLyK,GAAK,GAIT,OAAOyB,GAUT1H,EAAKwG,OAAOpJ,UAAU2K,WAAa,SAAUN,GAC3C,OAAO3G,KAAK0G,IAAIC,GAAe3G,KAAKsG,aAAe,GAQrDpH,EAAKwG,OAAOpJ,UAAU4K,QAAU,WAG9B,IAFA,IAAIC,EAAS,IAAI1G,MAAOT,KAAKwB,SAASrE,OAAS,GAEtCzC,EAAI,EAAGyK,EAAI,EAAGzK,EAAIsF,KAAKwB,SAASrE,OAAQzC,GAAK,EAAGyK,IACvDgC,EAAOhC,GAAKnF,KAAKwB,SAAS9G,GAG5B,OAAOyM,GAQTjI,EAAKwG,OAAOpJ,UAAUmJ,OAAS,WAC7B,OAAOzF,KAAKwB;;;;;IAoBdtC,EAAKQ,SACCjC,EAAY,CACZ,QAAY,MACZ,OAAW,OACX,KAAS,OACT,KAAS,OACT,KAAS,MACT,IAAQ,MACR,KAAS,KACT,MAAU,MACV,IAAQ,IACR,MAAU,MACV,QAAY,MACZ,MAAU,MACV,KAAS,MACT,MAAU,KACV,QAAY,MACZ,QAAY,MACZ,QAAY,MACZ,MAAU,KACV,MAAU,MACV,OAAW,MACX,KAAS,OAGXC,EAAY,CACV,MAAU,KACV,MAAU,GACV,MAAU,KACV,MAAU,KACV,KAAS,KACT,IAAQ,GACR,KAAS,IAIXC,EAAI,WACJC,EAAI7C,qBAQF8C,EAAU,IAAIuJ,OALT,4DAMLtJ,EAAU,IAAIsJ,OAJT,8FAKLrJ,EAAU,IAAIqJ,OANT,gFAOLpJ,EAAS,IAAIoJ,OALT,kCAOJnJ,EAAQ,kBACRC,EAAS,iBACTC,EAAQ,aACRC,EAAS,kBACTC,EAAU,KACVC,EAAW,cACXC,EAAW,IAAI6I,OAAO,sBACtB5I,EAAW,IAAI4I,OAAO,IAAMxJ,EAAID,EAAI,gBAEpCc,EAAQ,mBACRC,EAAO,2IAEPC,EAAO,iDAEPC,EAAO,sFACPC,EAAQ,oBAERC,EAAO,WACPC,EAAS,MACTC,EAAQ,IAAIoI,OAAO,IAAMxJ,EAAID,EAAI,gBAEjCsB,EAAgB,SAAuBoI,GACzC,IAAIC,EACFC,EACAC,EACAC,EACAC,EACAC,EACAC,EAEF,GAAIP,EAAElK,OAAS,EAAK,OAAOkK,EAiB3B,GAde,MADfG,EAAUH,EAAEQ,OAAO,EAAE,MAEnBR,EAAIG,EAAQM,cAAgBT,EAAEQ,OAAO,IAKvCH,EAAMxJ,GADNuJ,EAAKxJ,GAGE8J,KAAKV,GAAMA,EAAIA,EAAEW,QAAQP,EAAG,QAC1BC,EAAIK,KAAKV,KAAMA,EAAIA,EAAEW,QAAQN,EAAI,SAI1CA,EAAMtJ,GADNqJ,EAAKtJ,GAEE4J,KAAKV,GAAI,CACd,IAAIY,EAAKR,EAAG1K,KAAKsK,IACjBI,EAAK5J,GACEkK,KAAKE,EAAG,MACbR,EAAKpJ,EACLgJ,EAAIA,EAAEW,QAAQP,EAAG,UAEVC,EAAIK,KAAKV,KAElBC,GADIW,EAAKP,EAAI3K,KAAKsK,IACR,IACVK,EAAM1J,GACE+J,KAAKT,KAGXK,EAAMpJ,EACNqJ,EAAMpJ,GAFNkJ,EAAMpJ,GAGEyJ,KAJRV,EAAIC,GAIeD,GAAQ,IAClBM,EAAII,KAAKV,IAAMI,EAAKpJ,EAASgJ,EAAIA,EAAEW,QAAQP,EAAG,KAC9CG,EAAIG,KAAKV,KAAMA,GAAQ,OAiFpC,OA5EAI,EAAKhJ,GACEsJ,KAAKV,KAGVA,GADAC,GADIW,EAAKR,EAAG1K,KAAKsK,IACP,IACC,MAIbI,EAAK/I,GACEqJ,KAAKV,KAEVC,GADIW,EAAKR,EAAG1K,KAAKsK,IACP,GACVE,EAASU,EAAG,IACZR,EAAK5J,GACEkK,KAAKT,KACVD,EAAIC,EAAO7J,EAAU8J,MAKzBE,EAAK9I,GACEoJ,KAAKV,KAEVC,GADIW,EAAKR,EAAG1K,KAAKsK,IACP,GACVE,EAASU,EAAG,IACZR,EAAK5J,GACEkK,KAAKT,KACVD,EAAIC,EAAO5J,EAAU6J,KAMzBG,EAAM7I,GADN4I,EAAK7I,GAEEmJ,KAAKV,IAEVC,GADIW,EAAKR,EAAG1K,KAAKsK,IACP,IACVI,EAAK3J,GACEiK,KAAKT,KACVD,EAAIC,IAEGI,EAAIK,KAAKV,KAElBC,GADIW,EAAKP,EAAI3K,KAAKsK,IACR,GAAKY,EAAG,IAClBP,EAAM5J,GACEiK,KAAKT,KACXD,EAAIC,KAKRG,EAAK3I,GACEiJ,KAAKV,KAEVC,GADIW,EAAKR,EAAG1K,KAAKsK,IACP,GAEVK,EAAM3J,EACN4J,EAAM3I,IAFNyI,EAAK3J,GAGEiK,KAAKT,IAAUI,EAAIK,KAAKT,KAAWK,EAAII,KAAKT,MACjDD,EAAIC,IAKRI,EAAM5J,GADN2J,EAAK1I,GAEEgJ,KAAKV,IAAMK,EAAIK,KAAKV,KACzBI,EAAKpJ,EACLgJ,EAAIA,EAAEW,QAAQP,EAAG,KAKJ,KAAXD,IACFH,EAAIG,EAAQrE,cAAgBkE,EAAEQ,OAAO,IAGhCR,GAGF,SAAU9B,GACf,OAAOA,EAAMxC,OAAO9D,KAIxBC,EAAK0E,SAASG,iBAAiB7E,EAAKQ,QAAS;;;;IAmB7CR,EAAKgJ,uBAAyB,SAAUC,GACtC,IAAIC,EAAQD,EAAUE,QAAO,SAAUnD,EAAMoD,GAE3C,OADApD,EAAKoD,GAAYA,EACVpD,IACN,IAEH,OAAO,SAAUK,GACf,GAAIA,GAAS6C,EAAM7C,EAAMlF,cAAgBkF,EAAMlF,WAAY,OAAOkF,IAiBtErG,EAAKO,eAAiBP,EAAKgJ,uBAAuB,CAChD,IACA,OACA,QACA,SACA,QACA,MACA,SACA,OACA,KACA,QACA,KACA,MACA,MACA,MACA,KACA,KACA,KACA,UACA,OACA,MACA,KACA,MACA,SACA,QACA,OACA,MACA,KACA,OACA,SACA,OACA,OACA,QACA,MACA,OACA,MACA,MACA,MACA,MACA,OACA,KACA,MACA,OACA,MACA,MACA,MACA,UACA,IACA,KACA,KACA,OACA,KACA,KACA,MACA,OACA,QACA,MACA,OACA,SACA,MACA,KACA,QACA,OACA,OACA,KACA,UACA,KACA,MACA,MACA,KACA,MACA,QACA,KACA,OACA,KACA,QACA,MACA,MACA,SACA,OACA,MACA,OACA,MACA,SACA,QACA,KACA,OACA,OACA,OACA,MACA,QACA,OACA,OACA,QACA,QACA,OACA,OACA,MACA,KACA,MACA,OACA,KACA,QACA,MACA,KACA,OACA,OACA,OACA,QACA,QACA,QACA,MACA,OACA,MACA,OACA,OACA,QACA,MACA,MACA,SAGFhJ,EAAK0E,SAASG,iBAAiB7E,EAAKO,eAAgB;;;;IAqBpDP,EAAKM,QAAU,SAAU+F,GACvB,OAAOA,EAAMxC,QAAO,SAAUtG,GAC5B,OAAOA,EAAEuL,QAAQ,OAAQ,IAAIA,QAAQ,OAAQ,QAIjD9I,EAAK0E,SAASG,iBAAiB7E,EAAKM,QAAS;;;;IA2B7CN,EAAKqJ,SAAW,WACdvI,KAAKwI,OAAQ,EACbxI,KAAKyI,MAAQ,GACbzI,KAAK0I,GAAKxJ,EAAKqJ,SAASI,QACxBzJ,EAAKqJ,SAASI,SAAW,GAW3BzJ,EAAKqJ,SAASI,QAAU,EASxBzJ,EAAKqJ,SAASK,UAAY,SAAUC,GAGlC,IAFA,IAAIzJ,EAAU,IAAIF,EAAKqJ,SAASlJ,QAEvB3E,EAAI,EAAG0I,EAAMyF,EAAI1L,OAAQzC,EAAI0I,EAAK1I,IACzC0E,EAAQ8G,OAAO2C,EAAInO,IAIrB,OADA0E,EAAQ0J,SACD1J,EAAQ2J,MAYjB7J,EAAKqJ,SAASS,WAAa,SAAUC,GACnC,MAAI,iBAAkBA,EACb/J,EAAKqJ,SAASW,gBAAgBD,EAAOE,KAAMF,EAAOG,cAElDlK,EAAKqJ,SAASpH,WAAW8H,EAAOE,OAmB3CjK,EAAKqJ,SAASW,gBAAkB,SAAUrM,EAAKuM,GAS7C,IARA,IAAIL,EAAO,IAAI7J,EAAKqJ,SAEhBc,EAAQ,CAAC,CACXC,KAAMP,EACNQ,eAAgBH,EAChBvM,IAAKA,IAGAwM,EAAMlM,QAAQ,CACnB,IAAIqM,EAAQH,EAAMI,MAGlB,GAAID,EAAM3M,IAAIM,OAAS,EAAG,CACxB,IACIuM,EADAC,EAAOH,EAAM3M,IAAI4G,OAAO,GAGxBkG,KAAQH,EAAMF,KAAKb,MACrBiB,EAAaF,EAAMF,KAAKb,MAAMkB,IAE9BD,EAAa,IAAIxK,EAAKqJ,SACtBiB,EAAMF,KAAKb,MAAMkB,GAAQD,GAGH,GAApBF,EAAM3M,IAAIM,SACZuM,EAAWlB,OAAQ,GAGrBa,EAAMlH,KAAK,CACTmH,KAAMI,EACNH,eAAgBC,EAAMD,eACtB1M,IAAK2M,EAAM3M,IAAI8D,MAAM,KAIzB,GAA4B,GAAxB6I,EAAMD,eAAV,CAKA,GAAI,MAAOC,EAAMF,KAAKb,MACpB,IAAImB,EAAgBJ,EAAMF,KAAKb,MAAM,SAChC,CACDmB,EAAgB,IAAI1K,EAAKqJ,SAC7BiB,EAAMF,KAAKb,MAAM,KAAOmB,EAiC1B,GA9BwB,GAApBJ,EAAM3M,IAAIM,SACZyM,EAAcpB,OAAQ,GAGxBa,EAAMlH,KAAK,CACTmH,KAAMM,EACNL,eAAgBC,EAAMD,eAAiB,EACvC1M,IAAK2M,EAAM3M,MAMT2M,EAAM3M,IAAIM,OAAS,GACrBkM,EAAMlH,KAAK,CACTmH,KAAME,EAAMF,KACZC,eAAgBC,EAAMD,eAAiB,EACvC1M,IAAK2M,EAAM3M,IAAI8D,MAAM,KAMD,GAApB6I,EAAM3M,IAAIM,SACZqM,EAAMF,KAAKd,OAAQ,GAMjBgB,EAAM3M,IAAIM,QAAU,EAAG,CACzB,GAAI,MAAOqM,EAAMF,KAAKb,MACpB,IAAIoB,EAAmBL,EAAMF,KAAKb,MAAM,SACnC,CACDoB,EAAmB,IAAI3K,EAAKqJ,SAChCiB,EAAMF,KAAKb,MAAM,KAAOoB,EAGF,GAApBL,EAAM3M,IAAIM,SACZ0M,EAAiBrB,OAAQ,GAG3Ba,EAAMlH,KAAK,CACTmH,KAAMO,EACNN,eAAgBC,EAAMD,eAAiB,EACvC1M,IAAK2M,EAAM3M,IAAI8D,MAAM,KAOzB,GAAI6I,EAAM3M,IAAIM,OAAS,EAAG,CACxB,IAEI2M,EAFAC,EAAQP,EAAM3M,IAAI4G,OAAO,GACzBuG,EAAQR,EAAM3M,IAAI4G,OAAO,GAGzBuG,KAASR,EAAMF,KAAKb,MACtBqB,EAAgBN,EAAMF,KAAKb,MAAMuB,IAEjCF,EAAgB,IAAI5K,EAAKqJ,SACzBiB,EAAMF,KAAKb,MAAMuB,GAASF,GAGJ,GAApBN,EAAM3M,IAAIM,SACZ2M,EAActB,OAAQ,GAGxBa,EAAMlH,KAAK,CACTmH,KAAMQ,EACNP,eAAgBC,EAAMD,eAAiB,EACvC1M,IAAKkN,EAAQP,EAAM3M,IAAI8D,MAAM,OAKnC,OAAOoI,GAaT7J,EAAKqJ,SAASpH,WAAa,SAAUtE,GAYnC,IAXA,IAAIyM,EAAO,IAAIpK,EAAKqJ,SAChBQ,EAAOO,EAUF5O,EAAI,EAAG0I,EAAMvG,EAAIM,OAAQzC,EAAI0I,EAAK1I,IAAK,CAC9C,IAAIiP,EAAO9M,EAAInC,GACX8N,EAAS9N,GAAK0I,EAAM,EAExB,GAAY,KAARuG,EACFL,EAAKb,MAAMkB,GAAQL,EACnBA,EAAKd,MAAQA,MAER,CACL,IAAIyB,EAAO,IAAI/K,EAAKqJ,SACpB0B,EAAKzB,MAAQA,EAEbc,EAAKb,MAAMkB,GAAQM,EACnBX,EAAOW,GAIX,OAAOlB,GAaT7J,EAAKqJ,SAASjM,UAAU4K,QAAU,WAQhC,IAPA,IAAIkB,EAAQ,GAERiB,EAAQ,CAAC,CACXa,OAAQ,GACRZ,KAAMtJ,OAGDqJ,EAAMlM,QAAQ,CACnB,IAAIqM,EAAQH,EAAMI,MACdhB,EAAQrN,OAAOmF,KAAKiJ,EAAMF,KAAKb,OAC/BrF,EAAMqF,EAAMtL,OAEZqM,EAAMF,KAAKd,QAKbgB,EAAMU,OAAOzG,OAAO,GACpB2E,EAAMjG,KAAKqH,EAAMU,SAGnB,IAAK,IAAIxP,EAAI,EAAGA,EAAI0I,EAAK1I,IAAK,CAC5B,IAAIyP,EAAO1B,EAAM/N,GAEjB2O,EAAMlH,KAAK,CACT+H,OAAQV,EAAMU,OAAO9H,OAAO+H,GAC5Bb,KAAME,EAAMF,KAAKb,MAAM0B,MAK7B,OAAO/B,GAaTlJ,EAAKqJ,SAASjM,UAAU+D,SAAW,WASjC,GAAIL,KAAKoK,KACP,OAAOpK,KAAKoK,KAOd,IAJA,IAAIvN,EAAMmD,KAAKwI,MAAQ,IAAM,IACzB6B,EAASjP,OAAOmF,KAAKP,KAAKyI,OAAO6B,OACjClH,EAAMiH,EAAOlN,OAERzC,EAAI,EAAGA,EAAI0I,EAAK1I,IAAK,CAC5B,IAAIsJ,EAAQqG,EAAO3P,GAGnBmC,EAAMA,EAAMmH,EAFDhE,KAAKyI,MAAMzE,GAEG0E,GAG3B,OAAO7L,GAaTqC,EAAKqJ,SAASjM,UAAUoF,UAAY,SAAUM,GAU5C,IATA,IAAImF,EAAS,IAAIjI,EAAKqJ,SAClBiB,OAAQlI,EAER+H,EAAQ,CAAC,CACXkB,MAAOvI,EACPmF,OAAQA,EACRmC,KAAMtJ,OAGDqJ,EAAMlM,QAAQ,CACnBqM,EAAQH,EAAMI,MAWd,IALA,IAAIe,EAASpP,OAAOmF,KAAKiJ,EAAMe,MAAM9B,OACjCgC,EAAOD,EAAOrN,OACduN,EAAStP,OAAOmF,KAAKiJ,EAAMF,KAAKb,OAChCkC,EAAOD,EAAOvN,OAETyN,EAAI,EAAGA,EAAIH,EAAMG,IAGxB,IAFA,IAAIC,EAAQL,EAAOI,GAEVzO,EAAI,EAAGA,EAAIwO,EAAMxO,IAAK,CAC7B,IAAI2O,EAAQJ,EAAOvO,GAEnB,GAAI2O,GAASD,GAAkB,KAATA,EAAc,CAClC,IAAIvB,EAAOE,EAAMF,KAAKb,MAAMqC,GACxBP,EAAQf,EAAMe,MAAM9B,MAAMoC,GAC1BrC,EAAQc,EAAKd,OAAS+B,EAAM/B,MAC5ByB,OAAO3I,EAEPwJ,KAAStB,EAAMrC,OAAOsB,OAIxBwB,EAAOT,EAAMrC,OAAOsB,MAAMqC,IACrBtC,MAAQyB,EAAKzB,OAASA,IAM3ByB,EAAO,IAAI/K,EAAKqJ,UACXC,MAAQA,EACbgB,EAAMrC,OAAOsB,MAAMqC,GAASb,GAG9BZ,EAAMlH,KAAK,CACToI,MAAOA,EACPpD,OAAQ8C,EACRX,KAAMA,MAOhB,OAAOnC,GAETjI,EAAKqJ,SAASlJ,QAAU,WACtBW,KAAK+K,aAAe,GACpB/K,KAAK+I,KAAO,IAAI7J,EAAKqJ,SACrBvI,KAAKgL,eAAiB,GACtBhL,KAAKiL,eAAiB,IAGxB/L,EAAKqJ,SAASlJ,QAAQ/C,UAAU4J,OAAS,SAAUgF,GACjD,IAAI5B,EACA6B,EAAe,EAEnB,GAAID,EAAOlL,KAAK+K,aACd,MAAM,IAAIzG,MAAO,+BAGnB,IAAK,IAAI5J,EAAI,EAAGA,EAAIwQ,EAAK/N,QAAUzC,EAAIsF,KAAK+K,aAAa5N,QACnD+N,EAAKxQ,IAAMsF,KAAK+K,aAAarQ,GAD8BA,IAE/DyQ,IAGFnL,KAAKoL,SAASD,GAGZ7B,EADgC,GAA9BtJ,KAAKgL,eAAe7N,OACf6C,KAAK+I,KAEL/I,KAAKgL,eAAehL,KAAKgL,eAAe7N,OAAS,GAAGkO,MAG7D,IAAS3Q,EAAIyQ,EAAczQ,EAAIwQ,EAAK/N,OAAQzC,IAAK,CAC/C,IAAI4Q,EAAW,IAAIpM,EAAKqJ,SACpBoB,EAAOuB,EAAKxQ,GAEhB4O,EAAKb,MAAMkB,GAAQ2B,EAEnBtL,KAAKgL,eAAe7I,KAAK,CACvBoJ,OAAQjC,EACRK,KAAMA,EACN0B,MAAOC,IAGThC,EAAOgC,EAGThC,EAAKd,OAAQ,EACbxI,KAAK+K,aAAeG,GAGtBhM,EAAKqJ,SAASlJ,QAAQ/C,UAAUwM,OAAS,WACvC9I,KAAKoL,SAAS,IAGhBlM,EAAKqJ,SAASlJ,QAAQ/C,UAAU8O,SAAW,SAAUI,GACnD,IAAK,IAAI9Q,EAAIsF,KAAKgL,eAAe7N,OAAS,EAAGzC,GAAK8Q,EAAQ9Q,IAAK,CAC7D,IAAI4O,EAAOtJ,KAAKgL,eAAetQ,GAC3B+Q,EAAWnC,EAAK+B,MAAMhL,WAEtBoL,KAAYzL,KAAKiL,eACnB3B,EAAKiC,OAAO9C,MAAMa,EAAKK,MAAQ3J,KAAKiL,eAAeQ,IAInDnC,EAAK+B,MAAMjB,KAAOqB,EAElBzL,KAAKiL,eAAeQ,GAAYnC,EAAK+B,OAGvCrL,KAAKgL,eAAevB;;;;IAwBxBvK,EAAKwM,MAAQ,SAAUC,GACrB3L,KAAK4L,cAAgBD,EAAMC,cAC3B5L,KAAK6L,aAAeF,EAAME,aAC1B7L,KAAK8L,SAAWH,EAAMG,SACtB9L,KAAK+L,OAASJ,EAAMI,OACpB/L,KAAKV,SAAWqM,EAAMrM,UA0ExBJ,EAAKwM,MAAMpP,UAAU0P,OAAS,SAAUC,GACtC,OAAOjM,KAAKkM,OAAM,SAAUA,GACb,IAAIhN,EAAKiN,YAAYF,EAAaC,GACxCE,YA6BXlN,EAAKwM,MAAMpP,UAAU4P,MAAQ,SAAUlJ,GAoBrC,IAZA,IAAIkJ,EAAQ,IAAIhN,EAAKmN,MAAMrM,KAAK+L,QAC5BO,EAAiBlR,OAAOY,OAAO,MAC/BuQ,EAAenR,OAAOY,OAAO,MAC7BwQ,EAAiBpR,OAAOY,OAAO,MAC/ByQ,EAAkBrR,OAAOY,OAAO,MAChC0Q,EAAoBtR,OAAOY,OAAO,MAO7BtB,EAAI,EAAGA,EAAIsF,KAAK+L,OAAO5O,OAAQzC,IACtC6R,EAAavM,KAAK+L,OAAOrR,IAAM,IAAIwE,EAAKwG,OAG1C1C,EAAGnI,KAAKqR,EAAOA,GAEf,IAASxR,EAAI,EAAGA,EAAIwR,EAAMS,QAAQxP,OAAQzC,IAAK,CAS7C,IAAIuO,EAASiD,EAAMS,QAAQjS,GACvBkS,EAAQ,KACRC,EAAgB3N,EAAKqC,IAAIO,MAG3B8K,EADE3D,EAAO6D,YACD9M,KAAKV,SAASgG,UAAU2D,EAAOE,KAAM,CAC3C4C,OAAQ9C,EAAO8C,SAGT,CAAC9C,EAAOE,MAGlB,IAAK,IAAIrO,EAAI,EAAGA,EAAI8R,EAAMzP,OAAQrC,IAAK,CACrC,IAAIqO,EAAOyD,EAAM9R,GAQjBmO,EAAOE,KAAOA,EAOd,IAAI4D,EAAe7N,EAAKqJ,SAASS,WAAWC,GACxC+D,EAAgBhN,KAAK8L,SAASpK,UAAUqL,GAAc7F,UAQ1D,GAA6B,IAAzB8F,EAAc7P,QAAgB8L,EAAOgE,WAAa/N,EAAKmN,MAAMY,SAASC,SAAU,CAClF,IAAK,IAAI7H,EAAI,EAAGA,EAAI4D,EAAO8C,OAAO5O,OAAQkI,IAAK,CAE7CoH,EADIU,EAAQlE,EAAO8C,OAAO1G,IACDnG,EAAKqC,IAAIO,MAGpC,MAGF,IAAK,IAAIqD,EAAI,EAAGA,EAAI6H,EAAc7P,OAAQgI,IAKxC,KAAIiI,EAAeJ,EAAc7H,GAC7B7C,EAAUtC,KAAK4L,cAAcwB,GAC7BC,EAAY/K,EAAQgL,OAExB,IAASjI,EAAI,EAAGA,EAAI4D,EAAO8C,OAAO5O,OAAQkI,IAAK,CAS7C,IACIkI,EAAejL,EADf6K,EAAQlE,EAAO8C,OAAO1G,IAEtBmI,EAAuBpS,OAAOmF,KAAKgN,GACnCE,EAAYL,EAAe,IAAMD,EACjCO,EAAuB,IAAIxO,EAAKqC,IAAIiM,GAoBxC,GAbIvE,EAAOgE,UAAY/N,EAAKmN,MAAMY,SAASC,WACzCL,EAAgBA,EAAcjL,MAAM8L,QAELpM,IAA3BmL,EAAgBU,KAClBV,EAAgBU,GAASjO,EAAKqC,IAAIE,WASlCwH,EAAOgE,UAAY/N,EAAKmN,MAAMY,SAASU,YA4B3C,GANApB,EAAaY,GAAO/G,OAAOiH,EAAWpE,EAAO2E,OAAO,SAAU7L,EAAGC,GAAK,OAAOD,EAAIC,MAM7EwK,EAAeiB,GAAnB,CAIA,IAAK,IAAI9S,EAAI,EAAGA,EAAI6S,EAAqBrQ,OAAQxC,IAAK,CAOpD,IAGIkT,EAHAC,EAAsBN,EAAqB7S,GAC3CoT,EAAmB,IAAI7O,EAAK2B,SAAUiN,EAAqBX,GAC3DrK,EAAWyK,EAAaO,QAG4BxM,KAAnDuM,EAAavB,EAAeyB,IAC/BzB,EAAeyB,GAAoB,IAAI7O,EAAK8O,UAAWZ,EAAcD,EAAOrK,GAE5E+K,EAAWtO,IAAI6N,EAAcD,EAAOrK,GAKxC0J,EAAeiB,IAAa,aAnDOnM,IAA7BoL,EAAkBS,KACpBT,EAAkBS,GAASjO,EAAKqC,IAAIO,OAGtC4K,EAAkBS,GAAST,EAAkBS,GAAOvL,MAAM8L,KA0DlE,GAAIzE,EAAOgE,WAAa/N,EAAKmN,MAAMY,SAASC,SAC1C,IAAS7H,EAAI,EAAGA,EAAI4D,EAAO8C,OAAO5O,OAAQkI,IAAK,CAE7CoH,EADIU,EAAQlE,EAAO8C,OAAO1G,IACDoH,EAAgBU,GAAOzL,UAAUmL,IAUhE,IAAIoB,EAAqB/O,EAAKqC,IAAIE,SAC9ByM,EAAuBhP,EAAKqC,IAAIO,MAEpC,IAASpH,EAAI,EAAGA,EAAIsF,KAAK+L,OAAO5O,OAAQzC,IAAK,CAC3C,IAAIyS,EAEAV,EAFAU,EAAQnN,KAAK+L,OAAOrR,MAGtBuT,EAAqBA,EAAmBvM,UAAU+K,EAAgBU,KAGhET,EAAkBS,KACpBe,EAAuBA,EAAqBtM,MAAM8K,EAAkBS,KAIxE,IAAIgB,EAAoB/S,OAAOmF,KAAK+L,GAChC8B,EAAU,GACVC,EAAUjT,OAAOY,OAAO,MAY5B,GAAIkQ,EAAMoC,YAAa,CACrBH,EAAoB/S,OAAOmF,KAAKP,KAAK6L,cAErC,IAASnR,EAAI,EAAGA,EAAIyT,EAAkBhR,OAAQzC,IAAK,CAC7CqT,EAAmBI,EAAkBzT,GAAzC,IACI2G,EAAWnC,EAAK2B,SAASM,WAAW4M,GACxCzB,EAAeyB,GAAoB,IAAI7O,EAAK8O,WAIhD,IAAStT,EAAI,EAAGA,EAAIyT,EAAkBhR,OAAQzC,IAAK,CASjD,IACIoG,GADAO,EAAWnC,EAAK2B,SAASM,WAAWgN,EAAkBzT,KACpCoG,OAEtB,GAAKmN,EAAmBpM,SAASf,KAI7BoN,EAAqBrM,SAASf,GAAlC,CAIA,IAEIyN,EAFAC,EAAcxO,KAAK6L,aAAaxK,GAChCoN,EAAQlC,EAAalL,EAASN,WAAWkG,WAAWuH,GAGxD,QAAqClN,KAAhCiN,EAAWF,EAAQvN,IACtByN,EAASE,OAASA,EAClBF,EAASG,UAAUC,QAAQrC,EAAejL,QACrC,CACL,IAAIvE,EAAQ,CACV8R,IAAK9N,EACL2N,MAAOA,EACPC,UAAWpC,EAAejL,IAE5BgN,EAAQvN,GAAUhE,EAClBsR,EAAQjM,KAAKrF,KAOjB,OAAOsR,EAAQ9D,MAAK,SAAUvI,EAAGC,GAC/B,OAAOA,EAAEyM,MAAQ1M,EAAE0M,UAYvBvP,EAAKwM,MAAMpP,UAAUmJ,OAAS,WAC5B,IAAImG,EAAgBxQ,OAAOmF,KAAKP,KAAK4L,eAClCtB,OACApH,KAAI,SAAUiG,GACb,MAAO,CAACA,EAAMnJ,KAAK4L,cAAczC,MAChCnJ,MAED6L,EAAezQ,OAAOmF,KAAKP,KAAK6L,cACjC3I,KAAI,SAAU0L,GACb,MAAO,CAACA,EAAK5O,KAAK6L,aAAa+C,GAAKnJ,YACnCzF,MAEL,MAAO,CACLH,QAASX,EAAKW,QACdkM,OAAQ/L,KAAK+L,OACbF,aAAcA,EACdD,cAAeA,EACftM,SAAUU,KAAKV,SAASmG,WAU5BvG,EAAKwM,MAAMxH,KAAO,SAAU2K,GAC1B,IAAIlD,EAAQ,GACRE,EAAe,GACfiD,EAAoBD,EAAgBhD,aACpCD,EAAgBxQ,OAAOY,OAAO,MAC9B+S,EAA0BF,EAAgBjD,cAC1CoD,EAAkB,IAAI9P,EAAKqJ,SAASlJ,QACpCC,EAAWJ,EAAK0E,SAASM,KAAK2K,EAAgBvP,UAE9CuP,EAAgBhP,SAAWX,EAAKW,SAClCX,EAAKY,MAAMC,KAAK,4EAA8Eb,EAAKW,QAAU,sCAAwCgP,EAAgBhP,QAAU,KAGjL,IAAK,IAAInF,EAAI,EAAGA,EAAIoU,EAAkB3R,OAAQzC,IAAK,CACjD,IACIkU,GADAK,EAAQH,EAAkBpU,IACd,GACZ8G,EAAWyN,EAAM,GAErBpD,EAAa+C,GAAO,IAAI1P,EAAKwG,OAAOlE,GAGtC,IAAS9G,EAAI,EAAGA,EAAIqU,EAAwB5R,OAAQzC,IAAK,CACvD,IAAIuU,EACA9F,GADA8F,EAAQF,EAAwBrU,IACnB,GACb4H,EAAU2M,EAAM,GAEpBD,EAAgB9I,OAAOiD,GACvByC,EAAczC,GAAQ7G,EAYxB,OATA0M,EAAgBlG,SAEhB6C,EAAMI,OAAS8C,EAAgB9C,OAE/BJ,EAAME,aAAeA,EACrBF,EAAMC,cAAgBA,EACtBD,EAAMG,SAAWkD,EAAgBjG,KACjC4C,EAAMrM,SAAWA,EAEV,IAAIJ,EAAKwM,MAAMC;;;;IA+BxBzM,EAAKG,QAAU,WACbW,KAAKkP,KAAO,KACZlP,KAAKmP,QAAU/T,OAAOY,OAAO,MAC7BgE,KAAKoP,WAAahU,OAAOY,OAAO,MAChCgE,KAAK4L,cAAgBxQ,OAAOY,OAAO,MACnCgE,KAAKqP,qBAAuB,GAC5BrP,KAAKsP,aAAe,GACpBtP,KAAKiD,UAAY/D,EAAK+D,UACtBjD,KAAKV,SAAW,IAAIJ,EAAK0E,SACzB5D,KAAKL,eAAiB,IAAIT,EAAK0E,SAC/B5D,KAAKuC,cAAgB,EACrBvC,KAAKuP,GAAK,IACVvP,KAAKwP,IAAM,IACXxP,KAAKqN,UAAY,EACjBrN,KAAKyP,kBAAoB,IAe3BvQ,EAAKG,QAAQ/C,UAAUsS,IAAM,SAAUA,GACrC5O,KAAKkP,KAAON,GAmCd1P,EAAKG,QAAQ/C,UAAU6Q,MAAQ,SAAUpM,EAAW2O,GAClD,GAAI,KAAK3H,KAAKhH,GACZ,MAAM,IAAI4O,WAAY,UAAY5O,EAAY,oCAGhDf,KAAKmP,QAAQpO,GAAa2O,GAAc,IAW1CxQ,EAAKG,QAAQ/C,UAAU0F,EAAI,SAAU4N,GAEjC5P,KAAKuP,GADHK,EAAS,EACD,EACDA,EAAS,EACR,EAEAA,GAWd1Q,EAAKG,QAAQ/C,UAAUuT,GAAK,SAAUD,GACpC5P,KAAKwP,IAAMI,GAoBb1Q,EAAKG,QAAQ/C,UAAUiD,IAAM,SAAUuQ,EAAKJ,GAC1C,IAAI5O,EAASgP,EAAI9P,KAAKkP,MAClBnD,EAAS3Q,OAAOmF,KAAKP,KAAKmP,SAE9BnP,KAAKoP,WAAWtO,GAAU4O,GAAc,GACxC1P,KAAKuC,eAAiB,EAEtB,IAAK,IAAI7H,EAAI,EAAGA,EAAIqR,EAAO5O,OAAQzC,IAAK,CACtC,IAAIqG,EAAYgL,EAAOrR,GACnBqV,EAAY/P,KAAKmP,QAAQpO,GAAWgP,UACpC5C,EAAQ4C,EAAYA,EAAUD,GAAOA,EAAI/O,GACzCsC,EAASrD,KAAKiD,UAAUkK,EAAO,CAC7BpB,OAAQ,CAAChL,KAEX6L,EAAQ5M,KAAKV,SAAS0F,IAAI3B,GAC1BhC,EAAW,IAAInC,EAAK2B,SAAUC,EAAQC,GACtCiP,EAAa5U,OAAOY,OAAO,MAE/BgE,KAAKqP,qBAAqBhO,GAAY2O,EACtChQ,KAAKsP,aAAajO,GAAY,EAG9BrB,KAAKsP,aAAajO,IAAauL,EAAMzP,OAGrC,IAAK,IAAIgI,EAAI,EAAGA,EAAIyH,EAAMzP,OAAQgI,IAAK,CACrC,IAAIgE,EAAOyD,EAAMzH,GAUjB,GARwB7D,MAApB0O,EAAW7G,KACb6G,EAAW7G,GAAQ,GAGrB6G,EAAW7G,IAAS,EAIY7H,MAA5BtB,KAAK4L,cAAczC,GAAoB,CACzC,IAAI7G,EAAUlH,OAAOY,OAAO,MAC5BsG,EAAgB,OAAItC,KAAKqN,UACzBrN,KAAKqN,WAAa,EAElB,IAAK,IAAIhI,EAAI,EAAGA,EAAI0G,EAAO5O,OAAQkI,IACjC/C,EAAQyJ,EAAO1G,IAAMjK,OAAOY,OAAO,MAGrCgE,KAAK4L,cAAczC,GAAQ7G,EAIsBhB,MAA/CtB,KAAK4L,cAAczC,GAAMpI,GAAWD,KACtCd,KAAK4L,cAAczC,GAAMpI,GAAWD,GAAU1F,OAAOY,OAAO,OAK9D,IAAK,IAAIrB,EAAI,EAAGA,EAAIqF,KAAKyP,kBAAkBtS,OAAQxC,IAAK,CACtD,IAAIsV,EAAcjQ,KAAKyP,kBAAkB9U,GACrCmI,EAAWqG,EAAKrG,SAASmN,GAEmC3O,MAA5DtB,KAAK4L,cAAczC,GAAMpI,GAAWD,GAAQmP,KAC9CjQ,KAAK4L,cAAczC,GAAMpI,GAAWD,GAAQmP,GAAe,IAG7DjQ,KAAK4L,cAAczC,GAAMpI,GAAWD,GAAQmP,GAAa9N,KAAKW,OAYtE5D,EAAKG,QAAQ/C,UAAU4T,6BAA+B,WAOpD,IALA,IAAIC,EAAY/U,OAAOmF,KAAKP,KAAKsP,cAC7Bc,EAAiBD,EAAUhT,OAC3BkT,EAAc,GACdC,EAAqB,GAEhB5V,EAAI,EAAGA,EAAI0V,EAAgB1V,IAAK,CACvC,IAAI2G,EAAWnC,EAAK2B,SAASM,WAAWgP,EAAUzV,IAC9CyS,EAAQ9L,EAASN,UAErBuP,EAAmBnD,KAAWmD,EAAmBnD,GAAS,GAC1DmD,EAAmBnD,IAAU,EAE7BkD,EAAYlD,KAAWkD,EAAYlD,GAAS,GAC5CkD,EAAYlD,IAAUnN,KAAKsP,aAAajO,GAG1C,IAAI0K,EAAS3Q,OAAOmF,KAAKP,KAAKmP,SAE9B,IAASzU,EAAI,EAAGA,EAAIqR,EAAO5O,OAAQzC,IAAK,CACtC,IAAIqG,EAAYgL,EAAOrR,GACvB2V,EAAYtP,GAAasP,EAAYtP,GAAauP,EAAmBvP,GAGvEf,KAAKuQ,mBAAqBF,GAQ5BnR,EAAKG,QAAQ/C,UAAUkU,mBAAqB,WAM1C,IALA,IAAI3E,EAAe,GACfsE,EAAY/U,OAAOmF,KAAKP,KAAKqP,sBAC7BoB,EAAkBN,EAAUhT,OAC5BuT,EAAetV,OAAOY,OAAO,MAExBtB,EAAI,EAAGA,EAAI+V,EAAiB/V,IAAK,CAaxC,IAZA,IAAI2G,EAAWnC,EAAK2B,SAASM,WAAWgP,EAAUzV,IAC9CqG,EAAYM,EAASN,UACrB4P,EAAc3Q,KAAKsP,aAAajO,GAChCmN,EAAc,IAAItP,EAAKwG,OACvBkL,EAAkB5Q,KAAKqP,qBAAqBhO,GAC5CuL,EAAQxR,OAAOmF,KAAKqQ,GACpBC,EAAcjE,EAAMzP,OAGpB2T,EAAa9Q,KAAKmP,QAAQpO,GAAW6M,OAAS,EAC9CmD,EAAW/Q,KAAKoP,WAAW/N,EAASP,QAAQ8M,OAAS,EAEhDzI,EAAI,EAAGA,EAAI0L,EAAa1L,IAAK,CACpC,IAGI9C,EAAKoM,EAAOuC,EAHZ7H,EAAOyD,EAAMzH,GACb8L,EAAKL,EAAgBzH,GACrBkE,EAAYrN,KAAK4L,cAAczC,GAAMmE,YAGdhM,IAAvBoP,EAAavH,IACf9G,EAAMnD,EAAKmD,IAAIrC,KAAK4L,cAAczC,GAAOnJ,KAAKuC,eAC9CmO,EAAavH,GAAQ9G,GAErBA,EAAMqO,EAAavH,GAGrBsF,EAAQpM,IAAQrC,KAAKwP,IAAM,GAAKyB,IAAOjR,KAAKwP,KAAO,EAAIxP,KAAKuP,GAAKvP,KAAKuP,IAAMoB,EAAc3Q,KAAKuQ,mBAAmBxP,KAAekQ,GACjIxC,GAASqC,EACTrC,GAASsC,EACTC,EAAqBtO,KAAKwO,MAAc,IAARzC,GAAgB,IAQhDD,EAAYtI,OAAOmH,EAAW2D,GAGhCnF,EAAaxK,GAAYmN,EAG3BxO,KAAK6L,aAAeA,GAQtB3M,EAAKG,QAAQ/C,UAAU6U,eAAiB,WACtCnR,KAAK8L,SAAW5M,EAAKqJ,SAASK,UAC5BxN,OAAOmF,KAAKP,KAAK4L,eAAetB,SAYpCpL,EAAKG,QAAQ/C,UAAUsD,MAAQ,WAK7B,OAJAI,KAAKkQ,+BACLlQ,KAAKwQ,qBACLxQ,KAAKmR,iBAEE,IAAIjS,EAAKwM,MAAM,CACpBE,cAAe5L,KAAK4L,cACpBC,aAAc7L,KAAK6L,aACnBC,SAAU9L,KAAK8L,SACfC,OAAQ3Q,OAAOmF,KAAKP,KAAKmP,SACzB7P,SAAUU,KAAKL,kBAkBnBT,EAAKG,QAAQ/C,UAAU8U,IAAM,SAAUpO,GACrC,IAAIqO,EAAO5Q,MAAMnE,UAAUqE,MAAM9F,KAAK2J,UAAW,GACjD6M,EAAKC,QAAQtR,MACbgD,EAAGuO,MAAMvR,KAAMqR,IAcjBnS,EAAK8O,UAAY,SAAU7E,EAAMgE,EAAOrK,GAStC,IARA,IAAI0O,EAAiBpW,OAAOY,OAAO,MAC/ByV,EAAerW,OAAOmF,KAAKuC,GAAY,IAOlCpI,EAAI,EAAGA,EAAI+W,EAAatU,OAAQzC,IAAK,CAC5C,IAAIuB,EAAMwV,EAAa/W,GACvB8W,EAAevV,GAAO6G,EAAS7G,GAAK0E,QAGtCX,KAAK8C,SAAW1H,OAAOY,OAAO,WAEjBsF,IAAT6H,IACFnJ,KAAK8C,SAASqG,GAAQ/N,OAAOY,OAAO,MACpCgE,KAAK8C,SAASqG,GAAMgE,GAASqE,IAajCtS,EAAK8O,UAAU1R,UAAUqS,QAAU,SAAU+C,GAG3C,IAFA,IAAI9E,EAAQxR,OAAOmF,KAAKmR,EAAe5O,UAE9BpI,EAAI,EAAGA,EAAIkS,EAAMzP,OAAQzC,IAAK,CACrC,IAAIyO,EAAOyD,EAAMlS,GACbqR,EAAS3Q,OAAOmF,KAAKmR,EAAe5O,SAASqG,IAEtB7H,MAAvBtB,KAAK8C,SAASqG,KAChBnJ,KAAK8C,SAASqG,GAAQ/N,OAAOY,OAAO,OAGtC,IAAK,IAAImJ,EAAI,EAAGA,EAAI4G,EAAO5O,OAAQgI,IAAK,CACtC,IAAIgI,EAAQpB,EAAO5G,GACf5E,EAAOnF,OAAOmF,KAAKmR,EAAe5O,SAASqG,GAAMgE,IAEnB7L,MAA9BtB,KAAK8C,SAASqG,GAAMgE,KACtBnN,KAAK8C,SAASqG,GAAMgE,GAAS/R,OAAOY,OAAO,OAG7C,IAAK,IAAIqJ,EAAI,EAAGA,EAAI9E,EAAKpD,OAAQkI,IAAK,CACpC,IAAIpJ,EAAMsE,EAAK8E,GAEwB/D,MAAnCtB,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAC7B+D,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAAOyV,EAAe5O,SAASqG,GAAMgE,GAAOlR,GAEvE+D,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAAO+D,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAAKmG,OAAOsP,EAAe5O,SAASqG,GAAMgE,GAAOlR,QAexHiD,EAAK8O,UAAU1R,UAAUiD,IAAM,SAAU4J,EAAMgE,EAAOrK,GACpD,KAAMqG,KAAQnJ,KAAK8C,UAGjB,OAFA9C,KAAK8C,SAASqG,GAAQ/N,OAAOY,OAAO,WACpCgE,KAAK8C,SAASqG,GAAMgE,GAASrK,GAI/B,GAAMqK,KAASnN,KAAK8C,SAASqG,GAO7B,IAFA,IAAIsI,EAAerW,OAAOmF,KAAKuC,GAEtBpI,EAAI,EAAGA,EAAI+W,EAAatU,OAAQzC,IAAK,CAC5C,IAAIuB,EAAMwV,EAAa/W,GAEnBuB,KAAO+D,KAAK8C,SAASqG,GAAMgE,GAC7BnN,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAAO+D,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAAKmG,OAAOU,EAAS7G,IAElF+D,KAAK8C,SAASqG,GAAMgE,GAAOlR,GAAO6G,EAAS7G,QAZ7C+D,KAAK8C,SAASqG,GAAMgE,GAASrK,GA2BjC5D,EAAKmN,MAAQ,SAAUsF,GACrB3R,KAAK2M,QAAU,GACf3M,KAAK2R,UAAYA,GA2BnBzS,EAAKmN,MAAMuF,SAAW,IAAIC,OAAQ,KAClC3S,EAAKmN,MAAMuF,SAASE,KAAO,EAC3B5S,EAAKmN,MAAMuF,SAASG,QAAU,EAC9B7S,EAAKmN,MAAMuF,SAASI,SAAW,EAa/B9S,EAAKmN,MAAMY,SAAW,CAIpBgF,SAAU,EAMV/E,SAAU,EAMVS,WAAY,GA0BdzO,EAAKmN,MAAM/P,UAAU2M,OAAS,SAAUA,GA+BtC,MA9BM,WAAYA,IAChBA,EAAO8C,OAAS/L,KAAK2R,WAGjB,UAAW1I,IACfA,EAAO2E,MAAQ,GAGX,gBAAiB3E,IACrBA,EAAO6D,aAAc,GAGjB,aAAc7D,IAClBA,EAAO2I,SAAW1S,EAAKmN,MAAMuF,SAASE,MAGnC7I,EAAO2I,SAAW1S,EAAKmN,MAAMuF,SAASG,SAAa9I,EAAOE,KAAK1F,OAAO,IAAMvE,EAAKmN,MAAMuF,WAC1F3I,EAAOE,KAAO,IAAMF,EAAOE,MAGxBF,EAAO2I,SAAW1S,EAAKmN,MAAMuF,SAASI,UAAc/I,EAAOE,KAAKxI,OAAO,IAAMzB,EAAKmN,MAAMuF,WAC3F3I,EAAOE,KAAYF,EAAOE,KAAO,KAG7B,aAAcF,IAClBA,EAAOgE,SAAW/N,EAAKmN,MAAMY,SAASgF,UAGxCjS,KAAK2M,QAAQxK,KAAK8G,GAEXjJ,MAUTd,EAAKmN,MAAM/P,UAAUgS,UAAY,WAC/B,IAAK,IAAI5T,EAAI,EAAGA,EAAIsF,KAAK2M,QAAQxP,OAAQzC,IACvC,GAAIsF,KAAK2M,QAAQjS,GAAGuS,UAAY/N,EAAKmN,MAAMY,SAASU,WAClD,OAAO,EAIX,OAAO,GA6BTzO,EAAKmN,MAAM/P,UAAU6M,KAAO,SAAUA,EAAM+I,GAC1C,GAAIzR,MAAMC,QAAQyI,GAEhB,OADAA,EAAK/E,SAAQ,SAAUxI,GAAKoE,KAAKmJ,KAAKvN,EAAGsD,EAAKY,MAAMQ,MAAM4R,MAAalS,MAChEA,KAGT,IAAIiJ,EAASiJ,GAAW,GAKxB,OAJAjJ,EAAOE,KAAOA,EAAK9I,WAEnBL,KAAKiJ,OAAOA,GAELjJ,MAETd,EAAKiT,gBAAkB,SAAUlS,EAAS4F,EAAOC,GAC/C9F,KAAK/E,KAAO,kBACZ+E,KAAKC,QAAUA,EACfD,KAAK6F,MAAQA,EACb7F,KAAK8F,IAAMA,GAGb5G,EAAKiT,gBAAgB7V,UAAY,IAAIgI,MACrCpF,EAAKkT,WAAa,SAAUvV,GAC1BmD,KAAKqS,QAAU,GACfrS,KAAKnD,IAAMA,EACXmD,KAAK7C,OAASN,EAAIM,OAClB6C,KAAK4E,IAAM,EACX5E,KAAK6F,MAAQ,EACb7F,KAAKsS,oBAAsB,IAG7BpT,EAAKkT,WAAW9V,UAAU0I,IAAM,WAG9B,IAFA,IAAIuN,EAAQrT,EAAKkT,WAAWI,QAErBD,GACLA,EAAQA,EAAMvS,OAIlBd,EAAKkT,WAAW9V,UAAUmW,YAAc,WAKtC,IAJA,IAAIC,EAAY,GACZnP,EAAavD,KAAK6F,MAClBvC,EAAWtD,KAAK4E,IAEXlK,EAAI,EAAGA,EAAIsF,KAAKsS,oBAAoBnV,OAAQzC,IACnD4I,EAAWtD,KAAKsS,oBAAoB5X,GACpCgY,EAAUvQ,KAAKnC,KAAKnD,IAAI8D,MAAM4C,EAAYD,IAC1CC,EAAaD,EAAW,EAM1B,OAHAoP,EAAUvQ,KAAKnC,KAAKnD,IAAI8D,MAAM4C,EAAYvD,KAAK4E,MAC/C5E,KAAKsS,oBAAoBnV,OAAS,EAE3BuV,EAAUC,KAAK,KAGxBzT,EAAKkT,WAAW9V,UAAUsW,KAAO,SAAUC,GACzC7S,KAAKqS,QAAQlQ,KAAK,CAChB0Q,KAAMA,EACNhW,IAAKmD,KAAKyS,cACV5M,MAAO7F,KAAK6F,MACZC,IAAK9F,KAAK4E,MAGZ5E,KAAK6F,MAAQ7F,KAAK4E,KAGpB1F,EAAKkT,WAAW9V,UAAUwW,gBAAkB,WAC1C9S,KAAKsS,oBAAoBnQ,KAAKnC,KAAK4E,IAAM,GACzC5E,KAAK4E,KAAO,GAGd1F,EAAKkT,WAAW9V,UAAU2N,KAAO,WAC/B,GAAIjK,KAAK4E,KAAO5E,KAAK7C,OACnB,OAAO+B,EAAKkT,WAAWW,IAGzB,IAAIpJ,EAAO3J,KAAKnD,IAAI4G,OAAOzD,KAAK4E,KAEhC,OADA5E,KAAK4E,KAAO,EACL+E,GAGTzK,EAAKkT,WAAW9V,UAAU0W,MAAQ,WAChC,OAAOhT,KAAK4E,IAAM5E,KAAK6F,OAGzB3G,EAAKkT,WAAW9V,UAAU2W,OAAS,WAC7BjT,KAAK6F,OAAS7F,KAAK4E,MACrB5E,KAAK4E,KAAO,GAGd5E,KAAK6F,MAAQ7F,KAAK4E,KAGpB1F,EAAKkT,WAAW9V,UAAU4W,OAAS,WACjClT,KAAK4E,KAAO,GAGd1F,EAAKkT,WAAW9V,UAAU6W,eAAiB,WACzC,IAAIxJ,EAAMyJ,EAEV,GAEEA,GADAzJ,EAAO3J,KAAKiK,QACI7M,WAAW,SACpBgW,EAAW,IAAMA,EAAW,IAEjCzJ,GAAQzK,EAAKkT,WAAWW,KAC1B/S,KAAKkT,UAIThU,EAAKkT,WAAW9V,UAAU+W,KAAO,WAC/B,OAAOrT,KAAK4E,IAAM5E,KAAK7C,QAGzB+B,EAAKkT,WAAWW,IAAM,MACtB7T,EAAKkT,WAAWkB,MAAQ,QACxBpU,EAAKkT,WAAWmB,KAAO,OACvBrU,EAAKkT,WAAWoB,cAAgB,gBAChCtU,EAAKkT,WAAWqB,MAAQ,QACxBvU,EAAKkT,WAAWsB,SAAW,WAE3BxU,EAAKkT,WAAWuB,SAAW,SAAUC,GAInC,OAHAA,EAAMV,SACNU,EAAMhB,KAAK1T,EAAKkT,WAAWkB,OAC3BM,EAAMX,SACC/T,EAAKkT,WAAWI,SAGzBtT,EAAKkT,WAAWyB,QAAU,SAAUD,GAQlC,GAPIA,EAAMZ,QAAU,IAClBY,EAAMV,SACNU,EAAMhB,KAAK1T,EAAKkT,WAAWmB,OAG7BK,EAAMX,SAEFW,EAAMP,OACR,OAAOnU,EAAKkT,WAAWI,SAI3BtT,EAAKkT,WAAW0B,gBAAkB,SAAUF,GAI1C,OAHAA,EAAMX,SACNW,EAAMT,iBACNS,EAAMhB,KAAK1T,EAAKkT,WAAWoB,eACpBtU,EAAKkT,WAAWI,SAGzBtT,EAAKkT,WAAW2B,SAAW,SAAUH,GAInC,OAHAA,EAAMX,SACNW,EAAMT,iBACNS,EAAMhB,KAAK1T,EAAKkT,WAAWqB,OACpBvU,EAAKkT,WAAWI,SAGzBtT,EAAKkT,WAAW4B,OAAS,SAAUJ,GAC7BA,EAAMZ,QAAU,GAClBY,EAAMhB,KAAK1T,EAAKkT,WAAWmB,OAe/BrU,EAAKkT,WAAW6B,cAAgB/U,EAAK+D,UAAUS,UAE/CxE,EAAKkT,WAAWI,QAAU,SAAUoB,GAClC,OAAa,CACX,IAAIjK,EAAOiK,EAAM3J,OAEjB,GAAIN,GAAQzK,EAAKkT,WAAWW,IAC1B,OAAO7T,EAAKkT,WAAW4B,OAIzB,GAA0B,IAAtBrK,EAAKvM,WAAW,GAApB,CAKA,GAAY,KAARuM,EACF,OAAOzK,EAAKkT,WAAWuB,SAGzB,GAAY,KAARhK,EAKF,OAJAiK,EAAMV,SACFU,EAAMZ,QAAU,GAClBY,EAAMhB,KAAK1T,EAAKkT,WAAWmB,MAEtBrU,EAAKkT,WAAW0B,gBAGzB,GAAY,KAARnK,EAKF,OAJAiK,EAAMV,SACFU,EAAMZ,QAAU,GAClBY,EAAMhB,KAAK1T,EAAKkT,WAAWmB,MAEtBrU,EAAKkT,WAAW2B,SAMzB,GAAY,KAARpK,GAAiC,IAAlBiK,EAAMZ,QAEvB,OADAY,EAAMhB,KAAK1T,EAAKkT,WAAWsB,UACpBxU,EAAKkT,WAAWI,QAMzB,GAAY,KAAR7I,GAAiC,IAAlBiK,EAAMZ,QAEvB,OADAY,EAAMhB,KAAK1T,EAAKkT,WAAWsB,UACpBxU,EAAKkT,WAAWI,QAGzB,GAAI7I,EAAK7M,MAAMoC,EAAKkT,WAAW6B,eAC7B,OAAO/U,EAAKkT,WAAWyB,aAzCvBD,EAAMd,oBA8CZ5T,EAAKiN,YAAc,SAAUtP,EAAKqP,GAChClM,KAAK4T,MAAQ,IAAI1U,EAAKkT,WAAYvV,GAClCmD,KAAKkM,MAAQA,EACblM,KAAKkU,cAAgB,GACrBlU,KAAKmU,UAAY,GAGnBjV,EAAKiN,YAAY7P,UAAU8P,MAAQ,WACjCpM,KAAK4T,MAAM5O,MACXhF,KAAKqS,QAAUrS,KAAK4T,MAAMvB,QAI1B,IAFA,IAAIE,EAAQrT,EAAKiN,YAAYiI,YAEtB7B,GACLA,EAAQA,EAAMvS,MAGhB,OAAOA,KAAKkM,OAGdhN,EAAKiN,YAAY7P,UAAU+X,WAAa,WACtC,OAAOrU,KAAKqS,QAAQrS,KAAKmU,YAG3BjV,EAAKiN,YAAY7P,UAAUgY,cAAgB,WACzC,IAAIC,EAASvU,KAAKqU,aAElB,OADArU,KAAKmU,WAAa,EACXI,GAGTrV,EAAKiN,YAAY7P,UAAUkY,WAAa,WACtC,IAAIC,EAAkBzU,KAAKkU,cAC3BlU,KAAKkM,MAAMjD,OAAOwL,GAClBzU,KAAKkU,cAAgB,IAGvBhV,EAAKiN,YAAYiI,YAAc,SAAUM,GACvC,IAAIH,EAASG,EAAOL,aAEpB,GAAc/S,MAAViT,EAIJ,OAAQA,EAAO1B,MACb,KAAK3T,EAAKkT,WAAWsB,SACnB,OAAOxU,EAAKiN,YAAYwI,cAC1B,KAAKzV,EAAKkT,WAAWkB,MACnB,OAAOpU,EAAKiN,YAAYyI,WAC1B,KAAK1V,EAAKkT,WAAWmB,KACnB,OAAOrU,EAAKiN,YAAY0I,UAC1B,QACE,IAAIC,EAAe,4CAA8CP,EAAO1B,KAMxE,MAJI0B,EAAO1X,IAAIM,QAAU,IACvB2X,GAAgB,gBAAkBP,EAAO1X,IAAM,KAG3C,IAAIqC,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,OAIzE5G,EAAKiN,YAAYwI,cAAgB,SAAUD,GACzC,IAAIH,EAASG,EAAOJ,gBAEpB,GAAchT,MAAViT,EAAJ,CAIA,OAAQA,EAAO1X,KACb,IAAK,IACH6X,EAAOR,cAAcjH,SAAW/N,EAAKmN,MAAMY,SAASU,WACpD,MACF,IAAK,IACH+G,EAAOR,cAAcjH,SAAW/N,EAAKmN,MAAMY,SAASC,SACpD,MACF,QACE,IAAI4H,EAAe,kCAAoCP,EAAO1X,IAAM,IACpE,MAAM,IAAIqC,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,KAGvE,IAAIiP,EAAaL,EAAOL,aAExB,GAAkB/S,MAAdyT,EAAyB,CACvBD,EAAe,yCACnB,MAAM,IAAI5V,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,KAGrE,OAAQiP,EAAWlC,MACjB,KAAK3T,EAAKkT,WAAWkB,MACnB,OAAOpU,EAAKiN,YAAYyI,WAC1B,KAAK1V,EAAKkT,WAAWmB,KACnB,OAAOrU,EAAKiN,YAAY0I,UAC1B,QACMC,EAAe,mCAAqCC,EAAWlC,KAAO,IAC1E,MAAM,IAAI3T,EAAKiT,gBAAiB2C,EAAcC,EAAWlP,MAAOkP,EAAWjP,QAIjF5G,EAAKiN,YAAYyI,WAAa,SAAUF,GACtC,IAAIH,EAASG,EAAOJ,gBAEpB,GAAchT,MAAViT,EAAJ,CAIA,IAAmD,GAA/CG,EAAOxI,MAAMyF,UAAUvQ,QAAQmT,EAAO1X,KAAY,CACpD,IAAImY,EAAiBN,EAAOxI,MAAMyF,UAAUzO,KAAI,SAAU+R,GAAK,MAAO,IAAMA,EAAI,OAAOtC,KAAK,MACxFmC,EAAe,uBAAyBP,EAAO1X,IAAM,uBAAyBmY,EAElF,MAAM,IAAI9V,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,KAGrE4O,EAAOR,cAAcnI,OAAS,CAACwI,EAAO1X,KAEtC,IAAIkY,EAAaL,EAAOL,aAExB,GAAkB/S,MAAdyT,EAAyB,CACvBD,EAAe,gCACnB,MAAM,IAAI5V,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,KAGrE,OAAQiP,EAAWlC,MACjB,KAAK3T,EAAKkT,WAAWmB,KACnB,OAAOrU,EAAKiN,YAAY0I,UAC1B,QACMC,EAAe,0BAA4BC,EAAWlC,KAAO,IACjE,MAAM,IAAI3T,EAAKiT,gBAAiB2C,EAAcC,EAAWlP,MAAOkP,EAAWjP,QAIjF5G,EAAKiN,YAAY0I,UAAY,SAAUH,GACrC,IAAIH,EAASG,EAAOJ,gBAEpB,GAAchT,MAAViT,EAAJ,CAIAG,EAAOR,cAAc/K,KAAOoL,EAAO1X,IAAIsG,eAEP,GAA5BoR,EAAO1X,IAAIuE,QAAQ,OACrBsT,EAAOR,cAAcpH,aAAc,GAGrC,IAAIiI,EAAaL,EAAOL,aAExB,GAAkB/S,MAAdyT,EAKJ,OAAQA,EAAWlC,MACjB,KAAK3T,EAAKkT,WAAWmB,KAEnB,OADAmB,EAAOF,aACAtV,EAAKiN,YAAY0I,UAC1B,KAAK3V,EAAKkT,WAAWkB,MAEnB,OADAoB,EAAOF,aACAtV,EAAKiN,YAAYyI,WAC1B,KAAK1V,EAAKkT,WAAWoB,cACnB,OAAOtU,EAAKiN,YAAY+I,kBAC1B,KAAKhW,EAAKkT,WAAWqB,MACnB,OAAOvU,EAAKiN,YAAYgJ,WAC1B,KAAKjW,EAAKkT,WAAWsB,SAEnB,OADAgB,EAAOF,aACAtV,EAAKiN,YAAYwI,cAC1B,QACE,IAAIG,EAAe,2BAA6BC,EAAWlC,KAAO,IAClE,MAAM,IAAI3T,EAAKiT,gBAAiB2C,EAAcC,EAAWlP,MAAOkP,EAAWjP,UApB7E4O,EAAOF,eAwBXtV,EAAKiN,YAAY+I,kBAAoB,SAAUR,GAC7C,IAAIH,EAASG,EAAOJ,gBAEpB,GAAchT,MAAViT,EAAJ,CAIA,IAAInL,EAAegM,SAASb,EAAO1X,IAAK,IAExC,GAAIwY,MAAMjM,GAAe,CACvB,IAAI0L,EAAe,gCACnB,MAAM,IAAI5V,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,KAGrE4O,EAAOR,cAAc9K,aAAeA,EAEpC,IAAI2L,EAAaL,EAAOL,aAExB,GAAkB/S,MAAdyT,EAKJ,OAAQA,EAAWlC,MACjB,KAAK3T,EAAKkT,WAAWmB,KAEnB,OADAmB,EAAOF,aACAtV,EAAKiN,YAAY0I,UAC1B,KAAK3V,EAAKkT,WAAWkB,MAEnB,OADAoB,EAAOF,aACAtV,EAAKiN,YAAYyI,WAC1B,KAAK1V,EAAKkT,WAAWoB,cACnB,OAAOtU,EAAKiN,YAAY+I,kBAC1B,KAAKhW,EAAKkT,WAAWqB,MACnB,OAAOvU,EAAKiN,YAAYgJ,WAC1B,KAAKjW,EAAKkT,WAAWsB,SAEnB,OADAgB,EAAOF,aACAtV,EAAKiN,YAAYwI,cAC1B,QACMG,EAAe,2BAA6BC,EAAWlC,KAAO,IAClE,MAAM,IAAI3T,EAAKiT,gBAAiB2C,EAAcC,EAAWlP,MAAOkP,EAAWjP,UApB7E4O,EAAOF,eAwBXtV,EAAKiN,YAAYgJ,WAAa,SAAUT,GACtC,IAAIH,EAASG,EAAOJ,gBAEpB,GAAchT,MAAViT,EAAJ,CAIA,IAAI3G,EAAQwH,SAASb,EAAO1X,IAAK,IAEjC,GAAIwY,MAAMzH,GAAQ,CAChB,IAAIkH,EAAe,wBACnB,MAAM,IAAI5V,EAAKiT,gBAAiB2C,EAAcP,EAAO1O,MAAO0O,EAAOzO,KAGrE4O,EAAOR,cAActG,MAAQA,EAE7B,IAAImH,EAAaL,EAAOL,aAExB,GAAkB/S,MAAdyT,EAKJ,OAAQA,EAAWlC,MACjB,KAAK3T,EAAKkT,WAAWmB,KAEnB,OADAmB,EAAOF,aACAtV,EAAKiN,YAAY0I,UAC1B,KAAK3V,EAAKkT,WAAWkB,MAEnB,OADAoB,EAAOF,aACAtV,EAAKiN,YAAYyI,WAC1B,KAAK1V,EAAKkT,WAAWoB,cACnB,OAAOtU,EAAKiN,YAAY+I,kBAC1B,KAAKhW,EAAKkT,WAAWqB,MACnB,OAAOvU,EAAKiN,YAAYgJ,WAC1B,KAAKjW,EAAKkT,WAAWsB,SAEnB,OADAgB,EAAOF,aACAtV,EAAKiN,YAAYwI,cAC1B,QACMG,EAAe,2BAA6BC,EAAWlC,KAAO,IAClE,MAAM,IAAI3T,EAAKiT,gBAAiB2C,EAAcC,EAAWlP,MAAOkP,EAAWjP,UApB7E4O,EAAOF,oBA+BS,0BAAd,EAYI,WAMN,OAAOtV,IAlBS,kCAx3GnB,I,8BCND,YAGAzE,EAAOD,QAAU,WACf,GAA0B,iBAAf8a,WACT,OAAOA,WAGT,IAAIC,EAEJ,IAGEA,EAAIvV,MAAQ,IAAIwV,SAAS,cAAb,GACZ,MAAOC,GAEP,GAAsB,iBAAXC,OACT,OAAOA,OAIT,GAAoB,iBAATC,KACT,OAAOA,KAIT,QAAsB,IAAXnY,EACT,OAAOA,EAIX,OAAO+X,EA5BQ,K,+BCHjB,IAAIA,EAGJA,EAAI,WACH,OAAOvV,KADJ,GAIJ,IAECuV,EAAIA,GAAK,IAAIC,SAAS,cAAb,GACR,MAAOC,GAEc,iBAAXC,SAAqBH,EAAIG,QAOrCjb,EAAOD,QAAU+a,G,4ECgDV,SAASK,EAAUC,EAASC,EAAYC,EAAGC,GAE9C,OAAO,IAAKD,IAAMA,EAAIE,WAAU,SAAUC,EAASC,GAC/C,SAASC,EAAUza,GAAS,IAAM0a,EAAKL,EAAU/L,KAAKtO,IAAW,MAAO8Z,GAAKU,EAAOV,IACpF,SAASa,EAAS3a,GAAS,IAAM0a,EAAKL,EAAiB,MAAEra,IAAW,MAAO8Z,GAAKU,EAAOV,IACvF,SAASY,EAAKjR,GAJlB,IAAezJ,EAIayJ,EAAOmR,KAAOL,EAAQ9Q,EAAOzJ,QAJ1CA,EAIyDyJ,EAAOzJ,MAJhDA,aAAiBoa,EAAIpa,EAAQ,IAAIoa,GAAE,SAAUG,GAAWA,EAAQva,OAIT6a,KAAKJ,EAAWE,GAClGD,GAAML,EAAYA,EAAUzE,MAAMsE,EAASC,GAAc,KAAK7L,WAgCzC7O,OAAOY,OA0FXZ,OAAOY,O,SCpKdya,E,OCyGX,MAAM,EA2BX,aAAmB,OAAEtX,EAAM,KAAEuX,EAAI,SAAEpX,EAAQ,MAAErC,IAC3C+C,KAAK2W,UC5GF,SACLD,GAEA,MAAMC,EAAY,IAAIC,IAChBC,EAAY,IAAItV,IACtB,IAAK,MAAMuO,KAAO4G,EAAM,CACtB,MAAOI,EAAMC,GAAQjH,EAAIkH,SAASC,MAAM,KAGlCD,EAAWlH,EAAIkH,SACfE,EAAWpH,EAAIoH,MAGfC,EAAO,EAAWrH,EAAIqH,MACzBnP,QAAQ,mBAAoB,IAC5BA,QAAQ,OAAQ,KAGnB,GAAI+O,EAAM,CACR,MAAMxL,EAASoL,EAAUpb,IAAIub,GAGxBD,EAAQO,IAAI7L,GASfoL,EAAUU,IAAIL,EAAU,CACtBA,WACAE,QACAC,OACA5L,YAZFA,EAAO2L,MAAQpH,EAAIoH,MACnB3L,EAAO4L,KAAQA,EAGfN,EAAQtX,IAAIgM,SAcdoL,EAAUU,IAAIL,EAAU,CACtBA,WACAE,QACAC,SAIN,OAAOR,ED4DYW,CAAuBZ,GACxC1W,KAAKuX,UE5GF,SACLpY,GAEA,MAAMuE,EAAY,IAAI0D,OAAOjI,EAAOuE,UAAW,OACzC6T,EAAY,CAACC,EAAYC,EAActO,IACpC,GAAGsO,4BAA+BtO,WAI3C,OAAQ+C,IACNA,EAAQA,EACLlE,QAAQ,gBAAiB,KACzB0P,OAGH,MAAM5a,EAAQ,IAAIsK,OAAO,MAAMjI,EAAOuE,cACpCwI,EACGlE,QAAQ,uBAAwB,QAChCA,QAAQtE,EAAW,QACnB,OAGL,OAAO/H,GAASA,EACbqM,QAAQlL,EAAOya,GACfvP,QAAQ,8BAA+B,OFoFzB2P,CAAuBxY,GAGxCD,KAAK+D,UAAUS,UAAY,IAAI0D,OAAOjI,EAAOuE,WAI3C1D,KAAK/C,WADc,IAAVA,EACIiC,MAAK,WAGW,IAAvBC,EAAOyY,KAAKza,QAAmC,OAAnBgC,EAAOyY,KAAK,GAC1C5X,KAAKoR,IAAKlS,KAAaC,EAAOyY,KAAK,KAC1BzY,EAAOyY,KAAKza,OAAS,GAC9B6C,KAAKoR,IAAKlS,KAAa2Y,iBAAiB1Y,EAAOyY,OAIjD,MAAMrT,EA/Dd,SAAoBxC,EAAaC,GAC/B,MAAOS,EAAGqV,GAAK,CAAC,IAAIvW,IAAIQ,GAAI,IAAIR,IAAIS,IACpC,MAAO,IACF,IAAIT,IAAI,IAAIkB,GAAGsV,OAAOpc,IAAUmc,EAAEV,IAAIzb,MA4DzBqc,CAAW,CACrB,UAAW,iBAAkB,WAC5B1Y,GAGH,IAAK,MAAMsY,KAAQzY,EAAOyY,KAAK1U,IAAI+U,GACpB,OAAbA,EAAoB/Y,KAAQA,KAAa+Y,IAEzC,IAAK,MAAMjV,KAAMuB,EACfvE,KAAKV,SAASyF,OAAO6S,EAAK5U,IAC1BhD,KAAKL,eAAeoF,OAAO6S,EAAK5U,IAKpChD,KAAKmN,MAAM,QAAS,CAAES,MAAO,MAC7B5N,KAAKmN,MAAM,QACXnN,KAAK4O,IAAI,YAGT,IAAK,MAAMkB,KAAO4G,EAChB1W,KAAKT,IAAIuQ,MAKA5Q,KAAKwM,MAAMxH,KAAKjH,GAoB1B,OAAOiP,GACZ,GAAIA,EACF,IACE,MAAMqL,EAAYvX,KAAKuX,UAAUrL,GAG3BS,EGtLP,SACLhR,GAEA,MAAMuQ,EAAS,IAAKhN,KAAamN,MAAM,CAAC,QAAS,SAKjD,OAJe,IAAKnN,KAAaiN,YAAYxQ,EAAOuQ,GAG7CE,QACAF,EAAMS,QH8KSuL,CAAiBhM,GAC9B6L,OAAO9O,GACNA,EAAOgE,WAAa/N,KAAKmN,MAAMY,SAASU,YA+C5C,MAAO,IA3CQ3N,KAAK/C,MAAM+O,OAAUE,EAAH,KAG9B7D,OAAqB,CAAC+F,GAAWQ,MAAKH,QAAOC,gBAC5C,MAAMyJ,EAAWnY,KAAK2W,UAAUpb,IAAIqT,GACpC,QAAwB,IAAbuJ,EAA0B,CACnC,MAAM,SAAEnB,EAAQ,MAAEE,EAAK,KAAEC,EAAI,OAAE5L,GAAW4M,EAGpCvL,EGlLb,SACLV,EAA4BU,GAE5B,MAAMD,EAAU,IAAIpL,IAAuB2K,GAGrC9G,EAA2B,GACjC,IAAK,IAAIxJ,EAAI,EAAGA,EAAIgR,EAAMzP,OAAQvB,IAChC,IAAK,MAAMqN,KAAU0D,EACfC,EAAMhR,GAAGwc,WAAWnP,EAAOE,QAC7B/D,EAAO6D,EAAOE,OAAQ,EACtBwD,EAAQ0L,OAAOpP,IAIrB,IAAK,MAAMA,KAAU0D,EACnBvH,EAAO6D,EAAOE,OAAQ,EAGxB,OAAO/D,EH+JmBkT,CACZ3L,EACAvR,OAAOmF,KAAKmO,EAAU5L,WAIlB8K,IAAUrC,IAAUnQ,OAAOmd,OAAO3L,GAAO4L,MAAM5c,GAAKA,GAC1DwS,EAAQjM,KAAK,CACX6U,WACAE,MAAOK,EAAUL,GACjBC,KAAMI,EAAUJ,GAChB1I,MAAOA,GAAS,EAAIb,GACpBhB,UAGJ,OAAOwB,GACN,IAGF9D,KAAK,CAACvI,EAAGC,IAAMA,EAAEyM,MAAQ1M,EAAE0M,OAG3BpG,OAAO,CAAC+F,EAAShJ,KAChB,MAAM+S,EAAWnY,KAAK2W,UAAUpb,IAAI6J,EAAO4R,UAC3C,QAAwB,IAAbmB,EAA0B,CACnC,MAAMvJ,EAAM,WAAYuJ,EACpBA,EAAS5M,OAAQyL,SACjBmB,EAASnB,SACb5I,EAAQiJ,IAAIzI,EAAK,IAAIR,EAAQ7S,IAAIqT,IAAQ,GAAIxJ,IAE/C,OAAOgJ,GACN,IAAIwI,KAGS2B,UAGlB,SAEArY,QAAQH,KAAK,kBAAkBmM,kCAKnC,MAAO,II9OX,IAAI,EAiEG,SAAeuM,EACpBxY,G,yCAEA,OAAQA,EAAQ4S,MAGd,KAAK4D,EAAkBiC,MAGrB,aArDN,SACEvZ,G,yCAEA,IAAIwZ,EAAO,UAGX,GAAsB,oBAAXpN,QAA0B,iBAAkBA,OAAQ,CAC7D,MAAMqN,EAAST,SAASU,cAAiC,gBAClD/B,GAAQ8B,EAAOE,IAAI7B,MAAM,WAGhC0B,EAAOA,EAAK3Q,QAAQ,KAAM8O,GAI5B,MAAMiC,EAAU,GAChB,IAAK,MAAMnB,KAAQzY,EAAOyY,KACX,OAATA,GAAemB,EAAQ5W,KAAQwW,EAAH,mBACnB,OAATf,GAAemB,EAAQ5W,KAAK,GAAGwW,cAAiBf,YAIlDzY,EAAOyY,KAAKza,OAAS,GACvB4b,EAAQ5W,KAAQwW,EAAH,0BAGXI,EAAQ5b,eACJ6b,cACDL,EAAH,sCACGI,OAsBGE,CAAqBhZ,EAAQwX,KAAKtY,QACxC,EAAQ,IAAI,EAAOc,EAAQwX,MACpB,CACL5E,KAAM4D,EAAkByC,OAI5B,KAAKzC,EAAkB0C,MACrB,MAAO,CACLtG,KAAM4D,EAAkB2C,OACxB3B,KAAM,EAAQ,EAAMzL,OAAO/L,EAAQwX,MAAQ,IAI/C,QACE,MAAM,IAAI7W,UAAU,6BL/G1B,SAAkB6V,GAChB,qBACA,qBACA,qBACA,uBAJF,CAAkBA,MAAiB,KKuHnC4C,iBAAiB,UAAiBC,GAAM,oCACtCC,kBAAkBd,EAAQa,EAAG7B","file":"assets/javascripts/worker/search.4ac00218.min.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 5);\n","/*!\n * escape-html\n * Copyright(c) 2012-2013 TJ Holowaychuk\n * Copyright(c) 2015 Andreas Lubbe\n * Copyright(c) 2015 Tiancheng \"Timothy\" Gu\n * MIT Licensed\n */\n\n'use strict';\n\n/**\n * Module variables.\n * @private\n */\n\nvar matchHtmlRegExp = /[\"'&<>]/;\n\n/**\n * Module exports.\n * @public\n */\n\nmodule.exports = escapeHtml;\n\n/**\n * Escape special characters in the given string of html.\n *\n * @param {string} string The string to escape for inserting into HTML\n * @return {string}\n * @public\n */\n\nfunction escapeHtml(string) {\n var str = '' + string;\n var match = matchHtmlRegExp.exec(str);\n\n if (!match) {\n return str;\n }\n\n var escape;\n var html = '';\n var index = 0;\n var lastIndex = 0;\n\n for (index = match.index; index < str.length; index++) {\n switch (str.charCodeAt(index)) {\n case 34: // \"\n escape = '"';\n break;\n case 38: // &\n escape = '&';\n break;\n case 39: // '\n escape = ''';\n break;\n case 60: // <\n escape = '<';\n break;\n case 62: // >\n escape = '>';\n break;\n default:\n continue;\n }\n\n if (lastIndex !== index) {\n html += str.substring(lastIndex, index);\n }\n\n lastIndex = index + 1;\n html += escape;\n }\n\n return lastIndex !== index\n ? html + str.substring(lastIndex, index)\n : html;\n}\n","var ___EXPOSE_LOADER_IMPORT___ = require(\"-!./lunr.js\");\nvar ___EXPOSE_LOADER_GET_GLOBAL_THIS___ = require(\"../expose-loader/dist/runtime/getGlobalThis.js\");\nvar ___EXPOSE_LOADER_GLOBAL_THIS___ = ___EXPOSE_LOADER_GET_GLOBAL_THIS___;\nif (typeof ___EXPOSE_LOADER_GLOBAL_THIS___[\"lunr\"] === 'undefined') ___EXPOSE_LOADER_GLOBAL_THIS___[\"lunr\"] = ___EXPOSE_LOADER_IMPORT___;\nmodule.exports = ___EXPOSE_LOADER_IMPORT___;\n","/**\n * lunr - http://lunrjs.com - A bit like Solr, but much smaller and not as bright - 2.3.9\n * Copyright (C) 2020 Oliver Nightingale\n * @license MIT\n */\n\n;(function(){\n\n/**\n * A convenience function for configuring and constructing\n * a new lunr Index.\n *\n * A lunr.Builder instance is created and the pipeline setup\n * with a trimmer, stop word filter and stemmer.\n *\n * This builder object is yielded to the configuration function\n * that is passed as a parameter, allowing the list of fields\n * and other builder parameters to be customised.\n *\n * All documents _must_ be added within the passed config function.\n *\n * @example\n * var idx = lunr(function () {\n * this.field('title')\n * this.field('body')\n * this.ref('id')\n *\n * documents.forEach(function (doc) {\n * this.add(doc)\n * }, this)\n * })\n *\n * @see {@link lunr.Builder}\n * @see {@link lunr.Pipeline}\n * @see {@link lunr.trimmer}\n * @see {@link lunr.stopWordFilter}\n * @see {@link lunr.stemmer}\n * @namespace {function} lunr\n */\nvar lunr = function (config) {\n var builder = new lunr.Builder\n\n builder.pipeline.add(\n lunr.trimmer,\n lunr.stopWordFilter,\n lunr.stemmer\n )\n\n builder.searchPipeline.add(\n lunr.stemmer\n )\n\n config.call(builder, builder)\n return builder.build()\n}\n\nlunr.version = \"2.3.9\"\n/*!\n * lunr.utils\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A namespace containing utils for the rest of the lunr library\n * @namespace lunr.utils\n */\nlunr.utils = {}\n\n/**\n * Print a warning message to the console.\n *\n * @param {String} message The message to be printed.\n * @memberOf lunr.utils\n * @function\n */\nlunr.utils.warn = (function (global) {\n /* eslint-disable no-console */\n return function (message) {\n if (global.console && console.warn) {\n console.warn(message)\n }\n }\n /* eslint-enable no-console */\n})(this)\n\n/**\n * Convert an object to a string.\n *\n * In the case of `null` and `undefined` the function returns\n * the empty string, in all other cases the result of calling\n * `toString` on the passed object is returned.\n *\n * @param {Any} obj The object to convert to a string.\n * @return {String} string representation of the passed object.\n * @memberOf lunr.utils\n */\nlunr.utils.asString = function (obj) {\n if (obj === void 0 || obj === null) {\n return \"\"\n } else {\n return obj.toString()\n }\n}\n\n/**\n * Clones an object.\n *\n * Will create a copy of an existing object such that any mutations\n * on the copy cannot affect the original.\n *\n * Only shallow objects are supported, passing a nested object to this\n * function will cause a TypeError.\n *\n * Objects with primitives, and arrays of primitives are supported.\n *\n * @param {Object} obj The object to clone.\n * @return {Object} a clone of the passed object.\n * @throws {TypeError} when a nested object is passed.\n * @memberOf Utils\n */\nlunr.utils.clone = function (obj) {\n if (obj === null || obj === undefined) {\n return obj\n }\n\n var clone = Object.create(null),\n keys = Object.keys(obj)\n\n for (var i = 0; i < keys.length; i++) {\n var key = keys[i],\n val = obj[key]\n\n if (Array.isArray(val)) {\n clone[key] = val.slice()\n continue\n }\n\n if (typeof val === 'string' ||\n typeof val === 'number' ||\n typeof val === 'boolean') {\n clone[key] = val\n continue\n }\n\n throw new TypeError(\"clone is not deep and does not support nested objects\")\n }\n\n return clone\n}\nlunr.FieldRef = function (docRef, fieldName, stringValue) {\n this.docRef = docRef\n this.fieldName = fieldName\n this._stringValue = stringValue\n}\n\nlunr.FieldRef.joiner = \"/\"\n\nlunr.FieldRef.fromString = function (s) {\n var n = s.indexOf(lunr.FieldRef.joiner)\n\n if (n === -1) {\n throw \"malformed field ref string\"\n }\n\n var fieldRef = s.slice(0, n),\n docRef = s.slice(n + 1)\n\n return new lunr.FieldRef (docRef, fieldRef, s)\n}\n\nlunr.FieldRef.prototype.toString = function () {\n if (this._stringValue == undefined) {\n this._stringValue = this.fieldName + lunr.FieldRef.joiner + this.docRef\n }\n\n return this._stringValue\n}\n/*!\n * lunr.Set\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A lunr set.\n *\n * @constructor\n */\nlunr.Set = function (elements) {\n this.elements = Object.create(null)\n\n if (elements) {\n this.length = elements.length\n\n for (var i = 0; i < this.length; i++) {\n this.elements[elements[i]] = true\n }\n } else {\n this.length = 0\n }\n}\n\n/**\n * A complete set that contains all elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.complete = {\n intersect: function (other) {\n return other\n },\n\n union: function () {\n return this\n },\n\n contains: function () {\n return true\n }\n}\n\n/**\n * An empty set that contains no elements.\n *\n * @static\n * @readonly\n * @type {lunr.Set}\n */\nlunr.Set.empty = {\n intersect: function () {\n return this\n },\n\n union: function (other) {\n return other\n },\n\n contains: function () {\n return false\n }\n}\n\n/**\n * Returns true if this set contains the specified object.\n *\n * @param {object} object - Object whose presence in this set is to be tested.\n * @returns {boolean} - True if this set contains the specified object.\n */\nlunr.Set.prototype.contains = function (object) {\n return !!this.elements[object]\n}\n\n/**\n * Returns a new set containing only the elements that are present in both\n * this set and the specified set.\n *\n * @param {lunr.Set} other - set to intersect with this set.\n * @returns {lunr.Set} a new set that is the intersection of this and the specified set.\n */\n\nlunr.Set.prototype.intersect = function (other) {\n var a, b, elements, intersection = []\n\n if (other === lunr.Set.complete) {\n return this\n }\n\n if (other === lunr.Set.empty) {\n return other\n }\n\n if (this.length < other.length) {\n a = this\n b = other\n } else {\n a = other\n b = this\n }\n\n elements = Object.keys(a.elements)\n\n for (var i = 0; i < elements.length; i++) {\n var element = elements[i]\n if (element in b.elements) {\n intersection.push(element)\n }\n }\n\n return new lunr.Set (intersection)\n}\n\n/**\n * Returns a new set combining the elements of this and the specified set.\n *\n * @param {lunr.Set} other - set to union with this set.\n * @return {lunr.Set} a new set that is the union of this and the specified set.\n */\n\nlunr.Set.prototype.union = function (other) {\n if (other === lunr.Set.complete) {\n return lunr.Set.complete\n }\n\n if (other === lunr.Set.empty) {\n return this\n }\n\n return new lunr.Set(Object.keys(this.elements).concat(Object.keys(other.elements)))\n}\n/**\n * A function to calculate the inverse document frequency for\n * a posting. This is shared between the builder and the index\n *\n * @private\n * @param {object} posting - The posting for a given term\n * @param {number} documentCount - The total number of documents.\n */\nlunr.idf = function (posting, documentCount) {\n var documentsWithTerm = 0\n\n for (var fieldName in posting) {\n if (fieldName == '_index') continue // Ignore the term index, its not a field\n documentsWithTerm += Object.keys(posting[fieldName]).length\n }\n\n var x = (documentCount - documentsWithTerm + 0.5) / (documentsWithTerm + 0.5)\n\n return Math.log(1 + Math.abs(x))\n}\n\n/**\n * A token wraps a string representation of a token\n * as it is passed through the text processing pipeline.\n *\n * @constructor\n * @param {string} [str=''] - The string token being wrapped.\n * @param {object} [metadata={}] - Metadata associated with this token.\n */\nlunr.Token = function (str, metadata) {\n this.str = str || \"\"\n this.metadata = metadata || {}\n}\n\n/**\n * Returns the token string that is being wrapped by this object.\n *\n * @returns {string}\n */\nlunr.Token.prototype.toString = function () {\n return this.str\n}\n\n/**\n * A token update function is used when updating or optionally\n * when cloning a token.\n *\n * @callback lunr.Token~updateFunction\n * @param {string} str - The string representation of the token.\n * @param {Object} metadata - All metadata associated with this token.\n */\n\n/**\n * Applies the given function to the wrapped string token.\n *\n * @example\n * token.update(function (str, metadata) {\n * return str.toUpperCase()\n * })\n *\n * @param {lunr.Token~updateFunction} fn - A function to apply to the token string.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.update = function (fn) {\n this.str = fn(this.str, this.metadata)\n return this\n}\n\n/**\n * Creates a clone of this token. Optionally a function can be\n * applied to the cloned token.\n *\n * @param {lunr.Token~updateFunction} [fn] - An optional function to apply to the cloned token.\n * @returns {lunr.Token}\n */\nlunr.Token.prototype.clone = function (fn) {\n fn = fn || function (s) { return s }\n return new lunr.Token (fn(this.str, this.metadata), this.metadata)\n}\n/*!\n * lunr.tokenizer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A function for splitting a string into tokens ready to be inserted into\n * the search index. Uses `lunr.tokenizer.separator` to split strings, change\n * the value of this property to change how strings are split into tokens.\n *\n * This tokenizer will convert its parameter to a string by calling `toString` and\n * then will split this string on the character in `lunr.tokenizer.separator`.\n * Arrays will have their elements converted to strings and wrapped in a lunr.Token.\n *\n * Optional metadata can be passed to the tokenizer, this metadata will be cloned and\n * added as metadata to every token that is created from the object to be tokenized.\n *\n * @static\n * @param {?(string|object|object[])} obj - The object to convert into tokens\n * @param {?object} metadata - Optional metadata to associate with every token\n * @returns {lunr.Token[]}\n * @see {@link lunr.Pipeline}\n */\nlunr.tokenizer = function (obj, metadata) {\n if (obj == null || obj == undefined) {\n return []\n }\n\n if (Array.isArray(obj)) {\n return obj.map(function (t) {\n return new lunr.Token(\n lunr.utils.asString(t).toLowerCase(),\n lunr.utils.clone(metadata)\n )\n })\n }\n\n var str = obj.toString().toLowerCase(),\n len = str.length,\n tokens = []\n\n for (var sliceEnd = 0, sliceStart = 0; sliceEnd <= len; sliceEnd++) {\n var char = str.charAt(sliceEnd),\n sliceLength = sliceEnd - sliceStart\n\n if ((char.match(lunr.tokenizer.separator) || sliceEnd == len)) {\n\n if (sliceLength > 0) {\n var tokenMetadata = lunr.utils.clone(metadata) || {}\n tokenMetadata[\"position\"] = [sliceStart, sliceLength]\n tokenMetadata[\"index\"] = tokens.length\n\n tokens.push(\n new lunr.Token (\n str.slice(sliceStart, sliceEnd),\n tokenMetadata\n )\n )\n }\n\n sliceStart = sliceEnd + 1\n }\n\n }\n\n return tokens\n}\n\n/**\n * The separator used to split a string into tokens. Override this property to change the behaviour of\n * `lunr.tokenizer` behaviour when tokenizing strings. By default this splits on whitespace and hyphens.\n *\n * @static\n * @see lunr.tokenizer\n */\nlunr.tokenizer.separator = /[\\s\\-]+/\n/*!\n * lunr.Pipeline\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Pipelines maintain an ordered list of functions to be applied to all\n * tokens in documents entering the search index and queries being ran against\n * the index.\n *\n * An instance of lunr.Index created with the lunr shortcut will contain a\n * pipeline with a stop word filter and an English language stemmer. Extra\n * functions can be added before or after either of these functions or these\n * default functions can be removed.\n *\n * When run the pipeline will call each function in turn, passing a token, the\n * index of that token in the original list of all tokens and finally a list of\n * all the original tokens.\n *\n * The output of functions in the pipeline will be passed to the next function\n * in the pipeline. To exclude a token from entering the index the function\n * should return undefined, the rest of the pipeline will not be called with\n * this token.\n *\n * For serialisation of pipelines to work, all functions used in an instance of\n * a pipeline should be registered with lunr.Pipeline. Registered functions can\n * then be loaded. If trying to load a serialised pipeline that uses functions\n * that are not registered an error will be thrown.\n *\n * If not planning on serialising the pipeline then registering pipeline functions\n * is not necessary.\n *\n * @constructor\n */\nlunr.Pipeline = function () {\n this._stack = []\n}\n\nlunr.Pipeline.registeredFunctions = Object.create(null)\n\n/**\n * A pipeline function maps lunr.Token to lunr.Token. A lunr.Token contains the token\n * string as well as all known metadata. A pipeline function can mutate the token string\n * or mutate (or add) metadata for a given token.\n *\n * A pipeline function can indicate that the passed token should be discarded by returning\n * null, undefined or an empty string. This token will not be passed to any downstream pipeline\n * functions and will not be added to the index.\n *\n * Multiple tokens can be returned by returning an array of tokens. Each token will be passed\n * to any downstream pipeline functions and all will returned tokens will be added to the index.\n *\n * Any number of pipeline functions may be chained together using a lunr.Pipeline.\n *\n * @interface lunr.PipelineFunction\n * @param {lunr.Token} token - A token from the document being processed.\n * @param {number} i - The index of this token in the complete list of tokens for this document/field.\n * @param {lunr.Token[]} tokens - All tokens for this document/field.\n * @returns {(?lunr.Token|lunr.Token[])}\n */\n\n/**\n * Register a function with the pipeline.\n *\n * Functions that are used in the pipeline should be registered if the pipeline\n * needs to be serialised, or a serialised pipeline needs to be loaded.\n *\n * Registering a function does not add it to a pipeline, functions must still be\n * added to instances of the pipeline for them to be used when running a pipeline.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @param {String} label - The label to register this function with\n */\nlunr.Pipeline.registerFunction = function (fn, label) {\n if (label in this.registeredFunctions) {\n lunr.utils.warn('Overwriting existing registered function: ' + label)\n }\n\n fn.label = label\n lunr.Pipeline.registeredFunctions[fn.label] = fn\n}\n\n/**\n * Warns if the function is not registered as a Pipeline function.\n *\n * @param {lunr.PipelineFunction} fn - The function to check for.\n * @private\n */\nlunr.Pipeline.warnIfFunctionNotRegistered = function (fn) {\n var isRegistered = fn.label && (fn.label in this.registeredFunctions)\n\n if (!isRegistered) {\n lunr.utils.warn('Function is not registered with pipeline. This may cause problems when serialising the index.\\n', fn)\n }\n}\n\n/**\n * Loads a previously serialised pipeline.\n *\n * All functions to be loaded must already be registered with lunr.Pipeline.\n * If any function from the serialised data has not been registered then an\n * error will be thrown.\n *\n * @param {Object} serialised - The serialised pipeline to load.\n * @returns {lunr.Pipeline}\n */\nlunr.Pipeline.load = function (serialised) {\n var pipeline = new lunr.Pipeline\n\n serialised.forEach(function (fnName) {\n var fn = lunr.Pipeline.registeredFunctions[fnName]\n\n if (fn) {\n pipeline.add(fn)\n } else {\n throw new Error('Cannot load unregistered function: ' + fnName)\n }\n })\n\n return pipeline\n}\n\n/**\n * Adds new functions to the end of the pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction[]} functions - Any number of functions to add to the pipeline.\n */\nlunr.Pipeline.prototype.add = function () {\n var fns = Array.prototype.slice.call(arguments)\n\n fns.forEach(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n this._stack.push(fn)\n }, this)\n}\n\n/**\n * Adds a single function after a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.after = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n pos = pos + 1\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Adds a single function before a function that already exists in the\n * pipeline.\n *\n * Logs a warning if the function has not been registered.\n *\n * @param {lunr.PipelineFunction} existingFn - A function that already exists in the pipeline.\n * @param {lunr.PipelineFunction} newFn - The new function to add to the pipeline.\n */\nlunr.Pipeline.prototype.before = function (existingFn, newFn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(newFn)\n\n var pos = this._stack.indexOf(existingFn)\n if (pos == -1) {\n throw new Error('Cannot find existingFn')\n }\n\n this._stack.splice(pos, 0, newFn)\n}\n\n/**\n * Removes a function from the pipeline.\n *\n * @param {lunr.PipelineFunction} fn The function to remove from the pipeline.\n */\nlunr.Pipeline.prototype.remove = function (fn) {\n var pos = this._stack.indexOf(fn)\n if (pos == -1) {\n return\n }\n\n this._stack.splice(pos, 1)\n}\n\n/**\n * Runs the current list of functions that make up the pipeline against the\n * passed tokens.\n *\n * @param {Array} tokens The tokens to run through the pipeline.\n * @returns {Array}\n */\nlunr.Pipeline.prototype.run = function (tokens) {\n var stackLength = this._stack.length\n\n for (var i = 0; i < stackLength; i++) {\n var fn = this._stack[i]\n var memo = []\n\n for (var j = 0; j < tokens.length; j++) {\n var result = fn(tokens[j], j, tokens)\n\n if (result === null || result === void 0 || result === '') continue\n\n if (Array.isArray(result)) {\n for (var k = 0; k < result.length; k++) {\n memo.push(result[k])\n }\n } else {\n memo.push(result)\n }\n }\n\n tokens = memo\n }\n\n return tokens\n}\n\n/**\n * Convenience method for passing a string through a pipeline and getting\n * strings out. This method takes care of wrapping the passed string in a\n * token and mapping the resulting tokens back to strings.\n *\n * @param {string} str - The string to pass through the pipeline.\n * @param {?object} metadata - Optional metadata to associate with the token\n * passed to the pipeline.\n * @returns {string[]}\n */\nlunr.Pipeline.prototype.runString = function (str, metadata) {\n var token = new lunr.Token (str, metadata)\n\n return this.run([token]).map(function (t) {\n return t.toString()\n })\n}\n\n/**\n * Resets the pipeline by removing any existing processors.\n *\n */\nlunr.Pipeline.prototype.reset = function () {\n this._stack = []\n}\n\n/**\n * Returns a representation of the pipeline ready for serialisation.\n *\n * Logs a warning if the function has not been registered.\n *\n * @returns {Array}\n */\nlunr.Pipeline.prototype.toJSON = function () {\n return this._stack.map(function (fn) {\n lunr.Pipeline.warnIfFunctionNotRegistered(fn)\n\n return fn.label\n })\n}\n/*!\n * lunr.Vector\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A vector is used to construct the vector space of documents and queries. These\n * vectors support operations to determine the similarity between two documents or\n * a document and a query.\n *\n * Normally no parameters are required for initializing a vector, but in the case of\n * loading a previously dumped vector the raw elements can be provided to the constructor.\n *\n * For performance reasons vectors are implemented with a flat array, where an elements\n * index is immediately followed by its value. E.g. [index, value, index, value]. This\n * allows the underlying array to be as sparse as possible and still offer decent\n * performance when being used for vector calculations.\n *\n * @constructor\n * @param {Number[]} [elements] - The flat list of element index and element value pairs.\n */\nlunr.Vector = function (elements) {\n this._magnitude = 0\n this.elements = elements || []\n}\n\n\n/**\n * Calculates the position within the vector to insert a given index.\n *\n * This is used internally by insert and upsert. If there are duplicate indexes then\n * the position is returned as if the value for that index were to be updated, but it\n * is the callers responsibility to check whether there is a duplicate at that index\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @returns {Number}\n */\nlunr.Vector.prototype.positionForIndex = function (index) {\n // For an empty vector the tuple can be inserted at the beginning\n if (this.elements.length == 0) {\n return 0\n }\n\n var start = 0,\n end = this.elements.length / 2,\n sliceLength = end - start,\n pivotPoint = Math.floor(sliceLength / 2),\n pivotIndex = this.elements[pivotPoint * 2]\n\n while (sliceLength > 1) {\n if (pivotIndex < index) {\n start = pivotPoint\n }\n\n if (pivotIndex > index) {\n end = pivotPoint\n }\n\n if (pivotIndex == index) {\n break\n }\n\n sliceLength = end - start\n pivotPoint = start + Math.floor(sliceLength / 2)\n pivotIndex = this.elements[pivotPoint * 2]\n }\n\n if (pivotIndex == index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex > index) {\n return pivotPoint * 2\n }\n\n if (pivotIndex < index) {\n return (pivotPoint + 1) * 2\n }\n}\n\n/**\n * Inserts an element at an index within the vector.\n *\n * Does not allow duplicates, will throw an error if there is already an entry\n * for this index.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n */\nlunr.Vector.prototype.insert = function (insertIdx, val) {\n this.upsert(insertIdx, val, function () {\n throw \"duplicate index\"\n })\n}\n\n/**\n * Inserts or updates an existing index within the vector.\n *\n * @param {Number} insertIdx - The index at which the element should be inserted.\n * @param {Number} val - The value to be inserted into the vector.\n * @param {function} fn - A function that is called for updates, the existing value and the\n * requested value are passed as arguments\n */\nlunr.Vector.prototype.upsert = function (insertIdx, val, fn) {\n this._magnitude = 0\n var position = this.positionForIndex(insertIdx)\n\n if (this.elements[position] == insertIdx) {\n this.elements[position + 1] = fn(this.elements[position + 1], val)\n } else {\n this.elements.splice(position, 0, insertIdx, val)\n }\n}\n\n/**\n * Calculates the magnitude of this vector.\n *\n * @returns {Number}\n */\nlunr.Vector.prototype.magnitude = function () {\n if (this._magnitude) return this._magnitude\n\n var sumOfSquares = 0,\n elementsLength = this.elements.length\n\n for (var i = 1; i < elementsLength; i += 2) {\n var val = this.elements[i]\n sumOfSquares += val * val\n }\n\n return this._magnitude = Math.sqrt(sumOfSquares)\n}\n\n/**\n * Calculates the dot product of this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The vector to compute the dot product with.\n * @returns {Number}\n */\nlunr.Vector.prototype.dot = function (otherVector) {\n var dotProduct = 0,\n a = this.elements, b = otherVector.elements,\n aLen = a.length, bLen = b.length,\n aVal = 0, bVal = 0,\n i = 0, j = 0\n\n while (i < aLen && j < bLen) {\n aVal = a[i], bVal = b[j]\n if (aVal < bVal) {\n i += 2\n } else if (aVal > bVal) {\n j += 2\n } else if (aVal == bVal) {\n dotProduct += a[i + 1] * b[j + 1]\n i += 2\n j += 2\n }\n }\n\n return dotProduct\n}\n\n/**\n * Calculates the similarity between this vector and another vector.\n *\n * @param {lunr.Vector} otherVector - The other vector to calculate the\n * similarity with.\n * @returns {Number}\n */\nlunr.Vector.prototype.similarity = function (otherVector) {\n return this.dot(otherVector) / this.magnitude() || 0\n}\n\n/**\n * Converts the vector to an array of the elements within the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toArray = function () {\n var output = new Array (this.elements.length / 2)\n\n for (var i = 1, j = 0; i < this.elements.length; i += 2, j++) {\n output[j] = this.elements[i]\n }\n\n return output\n}\n\n/**\n * A JSON serializable representation of the vector.\n *\n * @returns {Number[]}\n */\nlunr.Vector.prototype.toJSON = function () {\n return this.elements\n}\n/* eslint-disable */\n/*!\n * lunr.stemmer\n * Copyright (C) 2020 Oliver Nightingale\n * Includes code from - http://tartarus.org/~martin/PorterStemmer/js.txt\n */\n\n/**\n * lunr.stemmer is an english language stemmer, this is a JavaScript\n * implementation of the PorterStemmer taken from http://tartarus.org/~martin\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token - The string to stem\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n * @function\n */\nlunr.stemmer = (function(){\n var step2list = {\n \"ational\" : \"ate\",\n \"tional\" : \"tion\",\n \"enci\" : \"ence\",\n \"anci\" : \"ance\",\n \"izer\" : \"ize\",\n \"bli\" : \"ble\",\n \"alli\" : \"al\",\n \"entli\" : \"ent\",\n \"eli\" : \"e\",\n \"ousli\" : \"ous\",\n \"ization\" : \"ize\",\n \"ation\" : \"ate\",\n \"ator\" : \"ate\",\n \"alism\" : \"al\",\n \"iveness\" : \"ive\",\n \"fulness\" : \"ful\",\n \"ousness\" : \"ous\",\n \"aliti\" : \"al\",\n \"iviti\" : \"ive\",\n \"biliti\" : \"ble\",\n \"logi\" : \"log\"\n },\n\n step3list = {\n \"icate\" : \"ic\",\n \"ative\" : \"\",\n \"alize\" : \"al\",\n \"iciti\" : \"ic\",\n \"ical\" : \"ic\",\n \"ful\" : \"\",\n \"ness\" : \"\"\n },\n\n c = \"[^aeiou]\", // consonant\n v = \"[aeiouy]\", // vowel\n C = c + \"[^aeiouy]*\", // consonant sequence\n V = v + \"[aeiou]*\", // vowel sequence\n\n mgr0 = \"^(\" + C + \")?\" + V + C, // [C]VC... is m>0\n meq1 = \"^(\" + C + \")?\" + V + C + \"(\" + V + \")?$\", // [C]VC[V] is m=1\n mgr1 = \"^(\" + C + \")?\" + V + C + V + C, // [C]VCVC... is m>1\n s_v = \"^(\" + C + \")?\" + v; // vowel in stem\n\n var re_mgr0 = new RegExp(mgr0);\n var re_mgr1 = new RegExp(mgr1);\n var re_meq1 = new RegExp(meq1);\n var re_s_v = new RegExp(s_v);\n\n var re_1a = /^(.+?)(ss|i)es$/;\n var re2_1a = /^(.+?)([^s])s$/;\n var re_1b = /^(.+?)eed$/;\n var re2_1b = /^(.+?)(ed|ing)$/;\n var re_1b_2 = /.$/;\n var re2_1b_2 = /(at|bl|iz)$/;\n var re3_1b_2 = new RegExp(\"([^aeiouylsz])\\\\1$\");\n var re4_1b_2 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var re_1c = /^(.+?[^aeiou])y$/;\n var re_2 = /^(.+?)(ational|tional|enci|anci|izer|bli|alli|entli|eli|ousli|ization|ation|ator|alism|iveness|fulness|ousness|aliti|iviti|biliti|logi)$/;\n\n var re_3 = /^(.+?)(icate|ative|alize|iciti|ical|ful|ness)$/;\n\n var re_4 = /^(.+?)(al|ance|ence|er|ic|able|ible|ant|ement|ment|ent|ou|ism|ate|iti|ous|ive|ize)$/;\n var re2_4 = /^(.+?)(s|t)(ion)$/;\n\n var re_5 = /^(.+?)e$/;\n var re_5_1 = /ll$/;\n var re3_5 = new RegExp(\"^\" + C + v + \"[^aeiouwxy]$\");\n\n var porterStemmer = function porterStemmer(w) {\n var stem,\n suffix,\n firstch,\n re,\n re2,\n re3,\n re4;\n\n if (w.length < 3) { return w; }\n\n firstch = w.substr(0,1);\n if (firstch == \"y\") {\n w = firstch.toUpperCase() + w.substr(1);\n }\n\n // Step 1a\n re = re_1a\n re2 = re2_1a;\n\n if (re.test(w)) { w = w.replace(re,\"$1$2\"); }\n else if (re2.test(w)) { w = w.replace(re2,\"$1$2\"); }\n\n // Step 1b\n re = re_1b;\n re2 = re2_1b;\n if (re.test(w)) {\n var fp = re.exec(w);\n re = re_mgr0;\n if (re.test(fp[1])) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1];\n re2 = re_s_v;\n if (re2.test(stem)) {\n w = stem;\n re2 = re2_1b_2;\n re3 = re3_1b_2;\n re4 = re4_1b_2;\n if (re2.test(w)) { w = w + \"e\"; }\n else if (re3.test(w)) { re = re_1b_2; w = w.replace(re,\"\"); }\n else if (re4.test(w)) { w = w + \"e\"; }\n }\n }\n\n // Step 1c - replace suffix y or Y by i if preceded by a non-vowel which is not the first letter of the word (so cry -> cri, by -> by, say -> say)\n re = re_1c;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n w = stem + \"i\";\n }\n\n // Step 2\n re = re_2;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step2list[suffix];\n }\n }\n\n // Step 3\n re = re_3;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n suffix = fp[2];\n re = re_mgr0;\n if (re.test(stem)) {\n w = stem + step3list[suffix];\n }\n }\n\n // Step 4\n re = re_4;\n re2 = re2_4;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n if (re.test(stem)) {\n w = stem;\n }\n } else if (re2.test(w)) {\n var fp = re2.exec(w);\n stem = fp[1] + fp[2];\n re2 = re_mgr1;\n if (re2.test(stem)) {\n w = stem;\n }\n }\n\n // Step 5\n re = re_5;\n if (re.test(w)) {\n var fp = re.exec(w);\n stem = fp[1];\n re = re_mgr1;\n re2 = re_meq1;\n re3 = re3_5;\n if (re.test(stem) || (re2.test(stem) && !(re3.test(stem)))) {\n w = stem;\n }\n }\n\n re = re_5_1;\n re2 = re_mgr1;\n if (re.test(w) && re2.test(w)) {\n re = re_1b_2;\n w = w.replace(re,\"\");\n }\n\n // and turn initial Y back to y\n\n if (firstch == \"y\") {\n w = firstch.toLowerCase() + w.substr(1);\n }\n\n return w;\n };\n\n return function (token) {\n return token.update(porterStemmer);\n }\n})();\n\nlunr.Pipeline.registerFunction(lunr.stemmer, 'stemmer')\n/*!\n * lunr.stopWordFilter\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.generateStopWordFilter builds a stopWordFilter function from the provided\n * list of stop words.\n *\n * The built in lunr.stopWordFilter is built using this generator and can be used\n * to generate custom stopWordFilters for applications or non English languages.\n *\n * @function\n * @param {Array} token The token to pass through the filter\n * @returns {lunr.PipelineFunction}\n * @see lunr.Pipeline\n * @see lunr.stopWordFilter\n */\nlunr.generateStopWordFilter = function (stopWords) {\n var words = stopWords.reduce(function (memo, stopWord) {\n memo[stopWord] = stopWord\n return memo\n }, {})\n\n return function (token) {\n if (token && words[token.toString()] !== token.toString()) return token\n }\n}\n\n/**\n * lunr.stopWordFilter is an English language stop word list filter, any words\n * contained in the list will not be passed through the filter.\n *\n * This is intended to be used in the Pipeline. If the token does not pass the\n * filter then undefined will be returned.\n *\n * @function\n * @implements {lunr.PipelineFunction}\n * @params {lunr.Token} token - A token to check for being a stop word.\n * @returns {lunr.Token}\n * @see {@link lunr.Pipeline}\n */\nlunr.stopWordFilter = lunr.generateStopWordFilter([\n 'a',\n 'able',\n 'about',\n 'across',\n 'after',\n 'all',\n 'almost',\n 'also',\n 'am',\n 'among',\n 'an',\n 'and',\n 'any',\n 'are',\n 'as',\n 'at',\n 'be',\n 'because',\n 'been',\n 'but',\n 'by',\n 'can',\n 'cannot',\n 'could',\n 'dear',\n 'did',\n 'do',\n 'does',\n 'either',\n 'else',\n 'ever',\n 'every',\n 'for',\n 'from',\n 'get',\n 'got',\n 'had',\n 'has',\n 'have',\n 'he',\n 'her',\n 'hers',\n 'him',\n 'his',\n 'how',\n 'however',\n 'i',\n 'if',\n 'in',\n 'into',\n 'is',\n 'it',\n 'its',\n 'just',\n 'least',\n 'let',\n 'like',\n 'likely',\n 'may',\n 'me',\n 'might',\n 'most',\n 'must',\n 'my',\n 'neither',\n 'no',\n 'nor',\n 'not',\n 'of',\n 'off',\n 'often',\n 'on',\n 'only',\n 'or',\n 'other',\n 'our',\n 'own',\n 'rather',\n 'said',\n 'say',\n 'says',\n 'she',\n 'should',\n 'since',\n 'so',\n 'some',\n 'than',\n 'that',\n 'the',\n 'their',\n 'them',\n 'then',\n 'there',\n 'these',\n 'they',\n 'this',\n 'tis',\n 'to',\n 'too',\n 'twas',\n 'us',\n 'wants',\n 'was',\n 'we',\n 'were',\n 'what',\n 'when',\n 'where',\n 'which',\n 'while',\n 'who',\n 'whom',\n 'why',\n 'will',\n 'with',\n 'would',\n 'yet',\n 'you',\n 'your'\n])\n\nlunr.Pipeline.registerFunction(lunr.stopWordFilter, 'stopWordFilter')\n/*!\n * lunr.trimmer\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.trimmer is a pipeline function for trimming non word\n * characters from the beginning and end of tokens before they\n * enter the index.\n *\n * This implementation may not work correctly for non latin\n * characters and should either be removed or adapted for use\n * with languages with non-latin characters.\n *\n * @static\n * @implements {lunr.PipelineFunction}\n * @param {lunr.Token} token The token to pass through the filter\n * @returns {lunr.Token}\n * @see lunr.Pipeline\n */\nlunr.trimmer = function (token) {\n return token.update(function (s) {\n return s.replace(/^\\W+/, '').replace(/\\W+$/, '')\n })\n}\n\nlunr.Pipeline.registerFunction(lunr.trimmer, 'trimmer')\n/*!\n * lunr.TokenSet\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * A token set is used to store the unique list of all tokens\n * within an index. Token sets are also used to represent an\n * incoming query to the index, this query token set and index\n * token set are then intersected to find which tokens to look\n * up in the inverted index.\n *\n * A token set can hold multiple tokens, as in the case of the\n * index token set, or it can hold a single token as in the\n * case of a simple query token set.\n *\n * Additionally token sets are used to perform wildcard matching.\n * Leading, contained and trailing wildcards are supported, and\n * from this edit distance matching can also be provided.\n *\n * Token sets are implemented as a minimal finite state automata,\n * where both common prefixes and suffixes are shared between tokens.\n * This helps to reduce the space used for storing the token set.\n *\n * @constructor\n */\nlunr.TokenSet = function () {\n this.final = false\n this.edges = {}\n this.id = lunr.TokenSet._nextId\n lunr.TokenSet._nextId += 1\n}\n\n/**\n * Keeps track of the next, auto increment, identifier to assign\n * to a new tokenSet.\n *\n * TokenSets require a unique identifier to be correctly minimised.\n *\n * @private\n */\nlunr.TokenSet._nextId = 1\n\n/**\n * Creates a TokenSet instance from the given sorted array of words.\n *\n * @param {String[]} arr - A sorted array of strings to create the set from.\n * @returns {lunr.TokenSet}\n * @throws Will throw an error if the input array is not sorted.\n */\nlunr.TokenSet.fromArray = function (arr) {\n var builder = new lunr.TokenSet.Builder\n\n for (var i = 0, len = arr.length; i < len; i++) {\n builder.insert(arr[i])\n }\n\n builder.finish()\n return builder.root\n}\n\n/**\n * Creates a token set from a query clause.\n *\n * @private\n * @param {Object} clause - A single clause from lunr.Query.\n * @param {string} clause.term - The query clause term.\n * @param {number} [clause.editDistance] - The optional edit distance for the term.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromClause = function (clause) {\n if ('editDistance' in clause) {\n return lunr.TokenSet.fromFuzzyString(clause.term, clause.editDistance)\n } else {\n return lunr.TokenSet.fromString(clause.term)\n }\n}\n\n/**\n * Creates a token set representing a single string with a specified\n * edit distance.\n *\n * Insertions, deletions, substitutions and transpositions are each\n * treated as an edit distance of 1.\n *\n * Increasing the allowed edit distance will have a dramatic impact\n * on the performance of both creating and intersecting these TokenSets.\n * It is advised to keep the edit distance less than 3.\n *\n * @param {string} str - The string to create the token set from.\n * @param {number} editDistance - The allowed edit distance to match.\n * @returns {lunr.Vector}\n */\nlunr.TokenSet.fromFuzzyString = function (str, editDistance) {\n var root = new lunr.TokenSet\n\n var stack = [{\n node: root,\n editsRemaining: editDistance,\n str: str\n }]\n\n while (stack.length) {\n var frame = stack.pop()\n\n // no edit\n if (frame.str.length > 0) {\n var char = frame.str.charAt(0),\n noEditNode\n\n if (char in frame.node.edges) {\n noEditNode = frame.node.edges[char]\n } else {\n noEditNode = new lunr.TokenSet\n frame.node.edges[char] = noEditNode\n }\n\n if (frame.str.length == 1) {\n noEditNode.final = true\n }\n\n stack.push({\n node: noEditNode,\n editsRemaining: frame.editsRemaining,\n str: frame.str.slice(1)\n })\n }\n\n if (frame.editsRemaining == 0) {\n continue\n }\n\n // insertion\n if (\"*\" in frame.node.edges) {\n var insertionNode = frame.node.edges[\"*\"]\n } else {\n var insertionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = insertionNode\n }\n\n if (frame.str.length == 0) {\n insertionNode.final = true\n }\n\n stack.push({\n node: insertionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str\n })\n\n // deletion\n // can only do a deletion if we have enough edits remaining\n // and if there are characters left to delete in the string\n if (frame.str.length > 1) {\n stack.push({\n node: frame.node,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // deletion\n // just removing the last character from the str\n if (frame.str.length == 1) {\n frame.node.final = true\n }\n\n // substitution\n // can only do a substitution if we have enough edits remaining\n // and if there are characters left to substitute\n if (frame.str.length >= 1) {\n if (\"*\" in frame.node.edges) {\n var substitutionNode = frame.node.edges[\"*\"]\n } else {\n var substitutionNode = new lunr.TokenSet\n frame.node.edges[\"*\"] = substitutionNode\n }\n\n if (frame.str.length == 1) {\n substitutionNode.final = true\n }\n\n stack.push({\n node: substitutionNode,\n editsRemaining: frame.editsRemaining - 1,\n str: frame.str.slice(1)\n })\n }\n\n // transposition\n // can only do a transposition if there are edits remaining\n // and there are enough characters to transpose\n if (frame.str.length > 1) {\n var charA = frame.str.charAt(0),\n charB = frame.str.charAt(1),\n transposeNode\n\n if (charB in frame.node.edges) {\n transposeNode = frame.node.edges[charB]\n } else {\n transposeNode = new lunr.TokenSet\n frame.node.edges[charB] = transposeNode\n }\n\n if (frame.str.length == 1) {\n transposeNode.final = true\n }\n\n stack.push({\n node: transposeNode,\n editsRemaining: frame.editsRemaining - 1,\n str: charA + frame.str.slice(2)\n })\n }\n }\n\n return root\n}\n\n/**\n * Creates a TokenSet from a string.\n *\n * The string may contain one or more wildcard characters (*)\n * that will allow wildcard matching when intersecting with\n * another TokenSet.\n *\n * @param {string} str - The string to create a TokenSet from.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.fromString = function (str) {\n var node = new lunr.TokenSet,\n root = node\n\n /*\n * Iterates through all characters within the passed string\n * appending a node for each character.\n *\n * When a wildcard character is found then a self\n * referencing edge is introduced to continually match\n * any number of any characters.\n */\n for (var i = 0, len = str.length; i < len; i++) {\n var char = str[i],\n final = (i == len - 1)\n\n if (char == \"*\") {\n node.edges[char] = node\n node.final = final\n\n } else {\n var next = new lunr.TokenSet\n next.final = final\n\n node.edges[char] = next\n node = next\n }\n }\n\n return root\n}\n\n/**\n * Converts this TokenSet into an array of strings\n * contained within the TokenSet.\n *\n * This is not intended to be used on a TokenSet that\n * contains wildcards, in these cases the results are\n * undefined and are likely to cause an infinite loop.\n *\n * @returns {string[]}\n */\nlunr.TokenSet.prototype.toArray = function () {\n var words = []\n\n var stack = [{\n prefix: \"\",\n node: this\n }]\n\n while (stack.length) {\n var frame = stack.pop(),\n edges = Object.keys(frame.node.edges),\n len = edges.length\n\n if (frame.node.final) {\n /* In Safari, at this point the prefix is sometimes corrupted, see:\n * https://github.com/olivernn/lunr.js/issues/279 Calling any\n * String.prototype method forces Safari to \"cast\" this string to what\n * it's supposed to be, fixing the bug. */\n frame.prefix.charAt(0)\n words.push(frame.prefix)\n }\n\n for (var i = 0; i < len; i++) {\n var edge = edges[i]\n\n stack.push({\n prefix: frame.prefix.concat(edge),\n node: frame.node.edges[edge]\n })\n }\n }\n\n return words\n}\n\n/**\n * Generates a string representation of a TokenSet.\n *\n * This is intended to allow TokenSets to be used as keys\n * in objects, largely to aid the construction and minimisation\n * of a TokenSet. As such it is not designed to be a human\n * friendly representation of the TokenSet.\n *\n * @returns {string}\n */\nlunr.TokenSet.prototype.toString = function () {\n // NOTE: Using Object.keys here as this.edges is very likely\n // to enter 'hash-mode' with many keys being added\n //\n // avoiding a for-in loop here as it leads to the function\n // being de-optimised (at least in V8). From some simple\n // benchmarks the performance is comparable, but allowing\n // V8 to optimize may mean easy performance wins in the future.\n\n if (this._str) {\n return this._str\n }\n\n var str = this.final ? '1' : '0',\n labels = Object.keys(this.edges).sort(),\n len = labels.length\n\n for (var i = 0; i < len; i++) {\n var label = labels[i],\n node = this.edges[label]\n\n str = str + label + node.id\n }\n\n return str\n}\n\n/**\n * Returns a new TokenSet that is the intersection of\n * this TokenSet and the passed TokenSet.\n *\n * This intersection will take into account any wildcards\n * contained within the TokenSet.\n *\n * @param {lunr.TokenSet} b - An other TokenSet to intersect with.\n * @returns {lunr.TokenSet}\n */\nlunr.TokenSet.prototype.intersect = function (b) {\n var output = new lunr.TokenSet,\n frame = undefined\n\n var stack = [{\n qNode: b,\n output: output,\n node: this\n }]\n\n while (stack.length) {\n frame = stack.pop()\n\n // NOTE: As with the #toString method, we are using\n // Object.keys and a for loop instead of a for-in loop\n // as both of these objects enter 'hash' mode, causing\n // the function to be de-optimised in V8\n var qEdges = Object.keys(frame.qNode.edges),\n qLen = qEdges.length,\n nEdges = Object.keys(frame.node.edges),\n nLen = nEdges.length\n\n for (var q = 0; q < qLen; q++) {\n var qEdge = qEdges[q]\n\n for (var n = 0; n < nLen; n++) {\n var nEdge = nEdges[n]\n\n if (nEdge == qEdge || qEdge == '*') {\n var node = frame.node.edges[nEdge],\n qNode = frame.qNode.edges[qEdge],\n final = node.final && qNode.final,\n next = undefined\n\n if (nEdge in frame.output.edges) {\n // an edge already exists for this character\n // no need to create a new node, just set the finality\n // bit unless this node is already final\n next = frame.output.edges[nEdge]\n next.final = next.final || final\n\n } else {\n // no edge exists yet, must create one\n // set the finality bit and insert it\n // into the output\n next = new lunr.TokenSet\n next.final = final\n frame.output.edges[nEdge] = next\n }\n\n stack.push({\n qNode: qNode,\n output: next,\n node: node\n })\n }\n }\n }\n }\n\n return output\n}\nlunr.TokenSet.Builder = function () {\n this.previousWord = \"\"\n this.root = new lunr.TokenSet\n this.uncheckedNodes = []\n this.minimizedNodes = {}\n}\n\nlunr.TokenSet.Builder.prototype.insert = function (word) {\n var node,\n commonPrefix = 0\n\n if (word < this.previousWord) {\n throw new Error (\"Out of order word insertion\")\n }\n\n for (var i = 0; i < word.length && i < this.previousWord.length; i++) {\n if (word[i] != this.previousWord[i]) break\n commonPrefix++\n }\n\n this.minimize(commonPrefix)\n\n if (this.uncheckedNodes.length == 0) {\n node = this.root\n } else {\n node = this.uncheckedNodes[this.uncheckedNodes.length - 1].child\n }\n\n for (var i = commonPrefix; i < word.length; i++) {\n var nextNode = new lunr.TokenSet,\n char = word[i]\n\n node.edges[char] = nextNode\n\n this.uncheckedNodes.push({\n parent: node,\n char: char,\n child: nextNode\n })\n\n node = nextNode\n }\n\n node.final = true\n this.previousWord = word\n}\n\nlunr.TokenSet.Builder.prototype.finish = function () {\n this.minimize(0)\n}\n\nlunr.TokenSet.Builder.prototype.minimize = function (downTo) {\n for (var i = this.uncheckedNodes.length - 1; i >= downTo; i--) {\n var node = this.uncheckedNodes[i],\n childKey = node.child.toString()\n\n if (childKey in this.minimizedNodes) {\n node.parent.edges[node.char] = this.minimizedNodes[childKey]\n } else {\n // Cache the key for this node since\n // we know it can't change anymore\n node.child._str = childKey\n\n this.minimizedNodes[childKey] = node.child\n }\n\n this.uncheckedNodes.pop()\n }\n}\n/*!\n * lunr.Index\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * An index contains the built index of all documents and provides a query interface\n * to the index.\n *\n * Usually instances of lunr.Index will not be created using this constructor, instead\n * lunr.Builder should be used to construct new indexes, or lunr.Index.load should be\n * used to load previously built and serialized indexes.\n *\n * @constructor\n * @param {Object} attrs - The attributes of the built search index.\n * @param {Object} attrs.invertedIndex - An index of term/field to document reference.\n * @param {Object} attrs.fieldVectors - Field vectors\n * @param {lunr.TokenSet} attrs.tokenSet - An set of all corpus tokens.\n * @param {string[]} attrs.fields - The names of indexed document fields.\n * @param {lunr.Pipeline} attrs.pipeline - The pipeline to use for search terms.\n */\nlunr.Index = function (attrs) {\n this.invertedIndex = attrs.invertedIndex\n this.fieldVectors = attrs.fieldVectors\n this.tokenSet = attrs.tokenSet\n this.fields = attrs.fields\n this.pipeline = attrs.pipeline\n}\n\n/**\n * A result contains details of a document matching a search query.\n * @typedef {Object} lunr.Index~Result\n * @property {string} ref - The reference of the document this result represents.\n * @property {number} score - A number between 0 and 1 representing how similar this document is to the query.\n * @property {lunr.MatchData} matchData - Contains metadata about this match including which term(s) caused the match.\n */\n\n/**\n * Although lunr provides the ability to create queries using lunr.Query, it also provides a simple\n * query language which itself is parsed into an instance of lunr.Query.\n *\n * For programmatically building queries it is advised to directly use lunr.Query, the query language\n * is best used for human entered text rather than program generated text.\n *\n * At its simplest queries can just be a single term, e.g. `hello`, multiple terms are also supported\n * and will be combined with OR, e.g `hello world` will match documents that contain either 'hello'\n * or 'world', though those that contain both will rank higher in the results.\n *\n * Wildcards can be included in terms to match one or more unspecified characters, these wildcards can\n * be inserted anywhere within the term, and more than one wildcard can exist in a single term. Adding\n * wildcards will increase the number of documents that will be found but can also have a negative\n * impact on query performance, especially with wildcards at the beginning of a term.\n *\n * Terms can be restricted to specific fields, e.g. `title:hello`, only documents with the term\n * hello in the title field will match this query. Using a field not present in the index will lead\n * to an error being thrown.\n *\n * Modifiers can also be added to terms, lunr supports edit distance and boost modifiers on terms. A term\n * boost will make documents matching that term score higher, e.g. `foo^5`. Edit distance is also supported\n * to provide fuzzy matching, e.g. 'hello~2' will match documents with hello with an edit distance of 2.\n * Avoid large values for edit distance to improve query performance.\n *\n * Each term also supports a presence modifier. By default a term's presence in document is optional, however\n * this can be changed to either required or prohibited. For a term's presence to be required in a document the\n * term should be prefixed with a '+', e.g. `+foo bar` is a search for documents that must contain 'foo' and\n * optionally contain 'bar'. Conversely a leading '-' sets the terms presence to prohibited, i.e. it must not\n * appear in a document, e.g. `-foo bar` is a search for documents that do not contain 'foo' but may contain 'bar'.\n *\n * To escape special characters the backslash character '\\' can be used, this allows searches to include\n * characters that would normally be considered modifiers, e.g. `foo\\~2` will search for a term \"foo~2\" instead\n * of attempting to apply a boost of 2 to the search term \"foo\".\n *\n * @typedef {string} lunr.Index~QueryString\n * @example Simple single term query\n * hello\n * @example Multiple term query\n * hello world\n * @example term scoped to a field\n * title:hello\n * @example term with a boost of 10\n * hello^10\n * @example term with an edit distance of 2\n * hello~2\n * @example terms with presence modifiers\n * -foo +bar baz\n */\n\n/**\n * Performs a search against the index using lunr query syntax.\n *\n * Results will be returned sorted by their score, the most relevant results\n * will be returned first. For details on how the score is calculated, please see\n * the {@link https://lunrjs.com/guides/searching.html#scoring|guide}.\n *\n * For more programmatic querying use lunr.Index#query.\n *\n * @param {lunr.Index~QueryString} queryString - A string containing a lunr query.\n * @throws {lunr.QueryParseError} If the passed query string cannot be parsed.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.search = function (queryString) {\n return this.query(function (query) {\n var parser = new lunr.QueryParser(queryString, query)\n parser.parse()\n })\n}\n\n/**\n * A query builder callback provides a query object to be used to express\n * the query to perform on the index.\n *\n * @callback lunr.Index~queryBuilder\n * @param {lunr.Query} query - The query object to build up.\n * @this lunr.Query\n */\n\n/**\n * Performs a query against the index using the yielded lunr.Query object.\n *\n * If performing programmatic queries against the index, this method is preferred\n * over lunr.Index#search so as to avoid the additional query parsing overhead.\n *\n * A query object is yielded to the supplied function which should be used to\n * express the query to be run against the index.\n *\n * Note that although this function takes a callback parameter it is _not_ an\n * asynchronous operation, the callback is just yielded a query object to be\n * customized.\n *\n * @param {lunr.Index~queryBuilder} fn - A function that is used to build the query.\n * @returns {lunr.Index~Result[]}\n */\nlunr.Index.prototype.query = function (fn) {\n // for each query clause\n // * process terms\n // * expand terms from token set\n // * find matching documents and metadata\n // * get document vectors\n // * score documents\n\n var query = new lunr.Query(this.fields),\n matchingFields = Object.create(null),\n queryVectors = Object.create(null),\n termFieldCache = Object.create(null),\n requiredMatches = Object.create(null),\n prohibitedMatches = Object.create(null)\n\n /*\n * To support field level boosts a query vector is created per\n * field. An empty vector is eagerly created to support negated\n * queries.\n */\n for (var i = 0; i < this.fields.length; i++) {\n queryVectors[this.fields[i]] = new lunr.Vector\n }\n\n fn.call(query, query)\n\n for (var i = 0; i < query.clauses.length; i++) {\n /*\n * Unless the pipeline has been disabled for this term, which is\n * the case for terms with wildcards, we need to pass the clause\n * term through the search pipeline. A pipeline returns an array\n * of processed terms. Pipeline functions may expand the passed\n * term, which means we may end up performing multiple index lookups\n * for a single query term.\n */\n var clause = query.clauses[i],\n terms = null,\n clauseMatches = lunr.Set.empty\n\n if (clause.usePipeline) {\n terms = this.pipeline.runString(clause.term, {\n fields: clause.fields\n })\n } else {\n terms = [clause.term]\n }\n\n for (var m = 0; m < terms.length; m++) {\n var term = terms[m]\n\n /*\n * Each term returned from the pipeline needs to use the same query\n * clause object, e.g. the same boost and or edit distance. The\n * simplest way to do this is to re-use the clause object but mutate\n * its term property.\n */\n clause.term = term\n\n /*\n * From the term in the clause we create a token set which will then\n * be used to intersect the indexes token set to get a list of terms\n * to lookup in the inverted index\n */\n var termTokenSet = lunr.TokenSet.fromClause(clause),\n expandedTerms = this.tokenSet.intersect(termTokenSet).toArray()\n\n /*\n * If a term marked as required does not exist in the tokenSet it is\n * impossible for the search to return any matches. We set all the field\n * scoped required matches set to empty and stop examining any further\n * clauses.\n */\n if (expandedTerms.length === 0 && clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = lunr.Set.empty\n }\n\n break\n }\n\n for (var j = 0; j < expandedTerms.length; j++) {\n /*\n * For each term get the posting and termIndex, this is required for\n * building the query vector.\n */\n var expandedTerm = expandedTerms[j],\n posting = this.invertedIndex[expandedTerm],\n termIndex = posting._index\n\n for (var k = 0; k < clause.fields.length; k++) {\n /*\n * For each field that this query term is scoped by (by default\n * all fields are in scope) we need to get all the document refs\n * that have this term in that field.\n *\n * The posting is the entry in the invertedIndex for the matching\n * term from above.\n */\n var field = clause.fields[k],\n fieldPosting = posting[field],\n matchingDocumentRefs = Object.keys(fieldPosting),\n termField = expandedTerm + \"/\" + field,\n matchingDocumentsSet = new lunr.Set(matchingDocumentRefs)\n\n /*\n * if the presence of this term is required ensure that the matching\n * documents are added to the set of required matches for this clause.\n *\n */\n if (clause.presence == lunr.Query.presence.REQUIRED) {\n clauseMatches = clauseMatches.union(matchingDocumentsSet)\n\n if (requiredMatches[field] === undefined) {\n requiredMatches[field] = lunr.Set.complete\n }\n }\n\n /*\n * if the presence of this term is prohibited ensure that the matching\n * documents are added to the set of prohibited matches for this field,\n * creating that set if it does not yet exist.\n */\n if (clause.presence == lunr.Query.presence.PROHIBITED) {\n if (prohibitedMatches[field] === undefined) {\n prohibitedMatches[field] = lunr.Set.empty\n }\n\n prohibitedMatches[field] = prohibitedMatches[field].union(matchingDocumentsSet)\n\n /*\n * Prohibited matches should not be part of the query vector used for\n * similarity scoring and no metadata should be extracted so we continue\n * to the next field\n */\n continue\n }\n\n /*\n * The query field vector is populated using the termIndex found for\n * the term and a unit value with the appropriate boost applied.\n * Using upsert because there could already be an entry in the vector\n * for the term we are working with. In that case we just add the scores\n * together.\n */\n queryVectors[field].upsert(termIndex, clause.boost, function (a, b) { return a + b })\n\n /**\n * If we've already seen this term, field combo then we've already collected\n * the matching documents and metadata, no need to go through all that again\n */\n if (termFieldCache[termField]) {\n continue\n }\n\n for (var l = 0; l < matchingDocumentRefs.length; l++) {\n /*\n * All metadata for this term/field/document triple\n * are then extracted and collected into an instance\n * of lunr.MatchData ready to be returned in the query\n * results\n */\n var matchingDocumentRef = matchingDocumentRefs[l],\n matchingFieldRef = new lunr.FieldRef (matchingDocumentRef, field),\n metadata = fieldPosting[matchingDocumentRef],\n fieldMatch\n\n if ((fieldMatch = matchingFields[matchingFieldRef]) === undefined) {\n matchingFields[matchingFieldRef] = new lunr.MatchData (expandedTerm, field, metadata)\n } else {\n fieldMatch.add(expandedTerm, field, metadata)\n }\n\n }\n\n termFieldCache[termField] = true\n }\n }\n }\n\n /**\n * If the presence was required we need to update the requiredMatches field sets.\n * We do this after all fields for the term have collected their matches because\n * the clause terms presence is required in _any_ of the fields not _all_ of the\n * fields.\n */\n if (clause.presence === lunr.Query.presence.REQUIRED) {\n for (var k = 0; k < clause.fields.length; k++) {\n var field = clause.fields[k]\n requiredMatches[field] = requiredMatches[field].intersect(clauseMatches)\n }\n }\n }\n\n /**\n * Need to combine the field scoped required and prohibited\n * matching documents into a global set of required and prohibited\n * matches\n */\n var allRequiredMatches = lunr.Set.complete,\n allProhibitedMatches = lunr.Set.empty\n\n for (var i = 0; i < this.fields.length; i++) {\n var field = this.fields[i]\n\n if (requiredMatches[field]) {\n allRequiredMatches = allRequiredMatches.intersect(requiredMatches[field])\n }\n\n if (prohibitedMatches[field]) {\n allProhibitedMatches = allProhibitedMatches.union(prohibitedMatches[field])\n }\n }\n\n var matchingFieldRefs = Object.keys(matchingFields),\n results = [],\n matches = Object.create(null)\n\n /*\n * If the query is negated (contains only prohibited terms)\n * we need to get _all_ fieldRefs currently existing in the\n * index. This is only done when we know that the query is\n * entirely prohibited terms to avoid any cost of getting all\n * fieldRefs unnecessarily.\n *\n * Additionally, blank MatchData must be created to correctly\n * populate the results.\n */\n if (query.isNegated()) {\n matchingFieldRefs = Object.keys(this.fieldVectors)\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n var matchingFieldRef = matchingFieldRefs[i]\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRef)\n matchingFields[matchingFieldRef] = new lunr.MatchData\n }\n }\n\n for (var i = 0; i < matchingFieldRefs.length; i++) {\n /*\n * Currently we have document fields that match the query, but we\n * need to return documents. The matchData and scores are combined\n * from multiple fields belonging to the same document.\n *\n * Scores are calculated by field, using the query vectors created\n * above, and combined into a final document score using addition.\n */\n var fieldRef = lunr.FieldRef.fromString(matchingFieldRefs[i]),\n docRef = fieldRef.docRef\n\n if (!allRequiredMatches.contains(docRef)) {\n continue\n }\n\n if (allProhibitedMatches.contains(docRef)) {\n continue\n }\n\n var fieldVector = this.fieldVectors[fieldRef],\n score = queryVectors[fieldRef.fieldName].similarity(fieldVector),\n docMatch\n\n if ((docMatch = matches[docRef]) !== undefined) {\n docMatch.score += score\n docMatch.matchData.combine(matchingFields[fieldRef])\n } else {\n var match = {\n ref: docRef,\n score: score,\n matchData: matchingFields[fieldRef]\n }\n matches[docRef] = match\n results.push(match)\n }\n }\n\n /*\n * Sort the results objects by score, highest first.\n */\n return results.sort(function (a, b) {\n return b.score - a.score\n })\n}\n\n/**\n * Prepares the index for JSON serialization.\n *\n * The schema for this JSON blob will be described in a\n * separate JSON schema file.\n *\n * @returns {Object}\n */\nlunr.Index.prototype.toJSON = function () {\n var invertedIndex = Object.keys(this.invertedIndex)\n .sort()\n .map(function (term) {\n return [term, this.invertedIndex[term]]\n }, this)\n\n var fieldVectors = Object.keys(this.fieldVectors)\n .map(function (ref) {\n return [ref, this.fieldVectors[ref].toJSON()]\n }, this)\n\n return {\n version: lunr.version,\n fields: this.fields,\n fieldVectors: fieldVectors,\n invertedIndex: invertedIndex,\n pipeline: this.pipeline.toJSON()\n }\n}\n\n/**\n * Loads a previously serialized lunr.Index\n *\n * @param {Object} serializedIndex - A previously serialized lunr.Index\n * @returns {lunr.Index}\n */\nlunr.Index.load = function (serializedIndex) {\n var attrs = {},\n fieldVectors = {},\n serializedVectors = serializedIndex.fieldVectors,\n invertedIndex = Object.create(null),\n serializedInvertedIndex = serializedIndex.invertedIndex,\n tokenSetBuilder = new lunr.TokenSet.Builder,\n pipeline = lunr.Pipeline.load(serializedIndex.pipeline)\n\n if (serializedIndex.version != lunr.version) {\n lunr.utils.warn(\"Version mismatch when loading serialised index. Current version of lunr '\" + lunr.version + \"' does not match serialized index '\" + serializedIndex.version + \"'\")\n }\n\n for (var i = 0; i < serializedVectors.length; i++) {\n var tuple = serializedVectors[i],\n ref = tuple[0],\n elements = tuple[1]\n\n fieldVectors[ref] = new lunr.Vector(elements)\n }\n\n for (var i = 0; i < serializedInvertedIndex.length; i++) {\n var tuple = serializedInvertedIndex[i],\n term = tuple[0],\n posting = tuple[1]\n\n tokenSetBuilder.insert(term)\n invertedIndex[term] = posting\n }\n\n tokenSetBuilder.finish()\n\n attrs.fields = serializedIndex.fields\n\n attrs.fieldVectors = fieldVectors\n attrs.invertedIndex = invertedIndex\n attrs.tokenSet = tokenSetBuilder.root\n attrs.pipeline = pipeline\n\n return new lunr.Index(attrs)\n}\n/*!\n * lunr.Builder\n * Copyright (C) 2020 Oliver Nightingale\n */\n\n/**\n * lunr.Builder performs indexing on a set of documents and\n * returns instances of lunr.Index ready for querying.\n *\n * All configuration of the index is done via the builder, the\n * fields to index, the document reference, the text processing\n * pipeline and document scoring parameters are all set on the\n * builder before indexing.\n *\n * @constructor\n * @property {string} _ref - Internal reference to the document reference field.\n * @property {string[]} _fields - Internal reference to the document fields to index.\n * @property {object} invertedIndex - The inverted index maps terms to document fields.\n * @property {object} documentTermFrequencies - Keeps track of document term frequencies.\n * @property {object} documentLengths - Keeps track of the length of documents added to the index.\n * @property {lunr.tokenizer} tokenizer - Function for splitting strings into tokens for indexing.\n * @property {lunr.Pipeline} pipeline - The pipeline performs text processing on tokens before indexing.\n * @property {lunr.Pipeline} searchPipeline - A pipeline for processing search terms before querying the index.\n * @property {number} documentCount - Keeps track of the total number of documents indexed.\n * @property {number} _b - A parameter to control field length normalization, setting this to 0 disabled normalization, 1 fully normalizes field lengths, the default value is 0.75.\n * @property {number} _k1 - A parameter to control how quickly an increase in term frequency results in term frequency saturation, the default value is 1.2.\n * @property {number} termIndex - A counter incremented for each unique term, used to identify a terms position in the vector space.\n * @property {array} metadataWhitelist - A list of metadata keys that have been whitelisted for entry in the index.\n */\nlunr.Builder = function () {\n this._ref = \"id\"\n this._fields = Object.create(null)\n this._documents = Object.create(null)\n this.invertedIndex = Object.create(null)\n this.fieldTermFrequencies = {}\n this.fieldLengths = {}\n this.tokenizer = lunr.tokenizer\n this.pipeline = new lunr.Pipeline\n this.searchPipeline = new lunr.Pipeline\n this.documentCount = 0\n this._b = 0.75\n this._k1 = 1.2\n this.termIndex = 0\n this.metadataWhitelist = []\n}\n\n/**\n * Sets the document field used as the document reference. Every document must have this field.\n * The type of this field in the document should be a string, if it is not a string it will be\n * coerced into a string by calling toString.\n *\n * The default ref is 'id'.\n *\n * The ref should _not_ be changed during indexing, it should be set before any documents are\n * added to the index. Changing it during indexing can lead to inconsistent results.\n *\n * @param {string} ref - The name of the reference field in the document.\n */\nlunr.Builder.prototype.ref = function (ref) {\n this._ref = ref\n}\n\n/**\n * A function that is used to extract a field from a document.\n *\n * Lunr expects a field to be at the top level of a document, if however the field\n * is deeply nested within a document an extractor function can be used to extract\n * the right field for indexing.\n *\n * @callback fieldExtractor\n * @param {object} doc - The document being added to the index.\n * @returns {?(string|object|object[])} obj - The object that will be indexed for this field.\n * @example Extracting a nested field\n * function (doc) { return doc.nested.field }\n */\n\n/**\n * Adds a field to the list of document fields that will be indexed. Every document being\n * indexed should have this field. Null values for this field in indexed documents will\n * not cause errors but will limit the chance of that document being retrieved by searches.\n *\n * All fields should be added before adding documents to the index. Adding fields after\n * a document has been indexed will have no effect on already indexed documents.\n *\n * Fields can be boosted at build time. This allows terms within that field to have more\n * importance when ranking search results. Use a field boost to specify that matches within\n * one field are more important than other fields.\n *\n * @param {string} fieldName - The name of a field to index in all documents.\n * @param {object} attributes - Optional attributes associated with this field.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this field.\n * @param {fieldExtractor} [attributes.extractor] - Function to extract a field from a document.\n * @throws {RangeError} fieldName cannot contain unsupported characters '/'\n */\nlunr.Builder.prototype.field = function (fieldName, attributes) {\n if (/\\//.test(fieldName)) {\n throw new RangeError (\"Field '\" + fieldName + \"' contains illegal character '/'\")\n }\n\n this._fields[fieldName] = attributes || {}\n}\n\n/**\n * A parameter to tune the amount of field length normalisation that is applied when\n * calculating relevance scores. A value of 0 will completely disable any normalisation\n * and a value of 1 will fully normalise field lengths. The default is 0.75. Values of b\n * will be clamped to the range 0 - 1.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.b = function (number) {\n if (number < 0) {\n this._b = 0\n } else if (number > 1) {\n this._b = 1\n } else {\n this._b = number\n }\n}\n\n/**\n * A parameter that controls the speed at which a rise in term frequency results in term\n * frequency saturation. The default value is 1.2. Setting this to a higher value will give\n * slower saturation levels, a lower value will result in quicker saturation.\n *\n * @param {number} number - The value to set for this tuning parameter.\n */\nlunr.Builder.prototype.k1 = function (number) {\n this._k1 = number\n}\n\n/**\n * Adds a document to the index.\n *\n * Before adding fields to the index the index should have been fully setup, with the document\n * ref and all fields to index already having been specified.\n *\n * The document must have a field name as specified by the ref (by default this is 'id') and\n * it should have all fields defined for indexing, though null or undefined values will not\n * cause errors.\n *\n * Entire documents can be boosted at build time. Applying a boost to a document indicates that\n * this document should rank higher in search results than other documents.\n *\n * @param {object} doc - The document to add to the index.\n * @param {object} attributes - Optional attributes associated with this document.\n * @param {number} [attributes.boost=1] - Boost applied to all terms within this document.\n */\nlunr.Builder.prototype.add = function (doc, attributes) {\n var docRef = doc[this._ref],\n fields = Object.keys(this._fields)\n\n this._documents[docRef] = attributes || {}\n this.documentCount += 1\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i],\n extractor = this._fields[fieldName].extractor,\n field = extractor ? extractor(doc) : doc[fieldName],\n tokens = this.tokenizer(field, {\n fields: [fieldName]\n }),\n terms = this.pipeline.run(tokens),\n fieldRef = new lunr.FieldRef (docRef, fieldName),\n fieldTerms = Object.create(null)\n\n this.fieldTermFrequencies[fieldRef] = fieldTerms\n this.fieldLengths[fieldRef] = 0\n\n // store the length of this field for this document\n this.fieldLengths[fieldRef] += terms.length\n\n // calculate term frequencies for this field\n for (var j = 0; j < terms.length; j++) {\n var term = terms[j]\n\n if (fieldTerms[term] == undefined) {\n fieldTerms[term] = 0\n }\n\n fieldTerms[term] += 1\n\n // add to inverted index\n // create an initial posting if one doesn't exist\n if (this.invertedIndex[term] == undefined) {\n var posting = Object.create(null)\n posting[\"_index\"] = this.termIndex\n this.termIndex += 1\n\n for (var k = 0; k < fields.length; k++) {\n posting[fields[k]] = Object.create(null)\n }\n\n this.invertedIndex[term] = posting\n }\n\n // add an entry for this term/fieldName/docRef to the invertedIndex\n if (this.invertedIndex[term][fieldName][docRef] == undefined) {\n this.invertedIndex[term][fieldName][docRef] = Object.create(null)\n }\n\n // store all whitelisted metadata about this token in the\n // inverted index\n for (var l = 0; l < this.metadataWhitelist.length; l++) {\n var metadataKey = this.metadataWhitelist[l],\n metadata = term.metadata[metadataKey]\n\n if (this.invertedIndex[term][fieldName][docRef][metadataKey] == undefined) {\n this.invertedIndex[term][fieldName][docRef][metadataKey] = []\n }\n\n this.invertedIndex[term][fieldName][docRef][metadataKey].push(metadata)\n }\n }\n\n }\n}\n\n/**\n * Calculates the average document length for this index\n *\n * @private\n */\nlunr.Builder.prototype.calculateAverageFieldLengths = function () {\n\n var fieldRefs = Object.keys(this.fieldLengths),\n numberOfFields = fieldRefs.length,\n accumulator = {},\n documentsWithField = {}\n\n for (var i = 0; i < numberOfFields; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n field = fieldRef.fieldName\n\n documentsWithField[field] || (documentsWithField[field] = 0)\n documentsWithField[field] += 1\n\n accumulator[field] || (accumulator[field] = 0)\n accumulator[field] += this.fieldLengths[fieldRef]\n }\n\n var fields = Object.keys(this._fields)\n\n for (var i = 0; i < fields.length; i++) {\n var fieldName = fields[i]\n accumulator[fieldName] = accumulator[fieldName] / documentsWithField[fieldName]\n }\n\n this.averageFieldLength = accumulator\n}\n\n/**\n * Builds a vector space model of every document using lunr.Vector\n *\n * @private\n */\nlunr.Builder.prototype.createFieldVectors = function () {\n var fieldVectors = {},\n fieldRefs = Object.keys(this.fieldTermFrequencies),\n fieldRefsLength = fieldRefs.length,\n termIdfCache = Object.create(null)\n\n for (var i = 0; i < fieldRefsLength; i++) {\n var fieldRef = lunr.FieldRef.fromString(fieldRefs[i]),\n fieldName = fieldRef.fieldName,\n fieldLength = this.fieldLengths[fieldRef],\n fieldVector = new lunr.Vector,\n termFrequencies = this.fieldTermFrequencies[fieldRef],\n terms = Object.keys(termFrequencies),\n termsLength = terms.length\n\n\n var fieldBoost = this._fields[fieldName].boost || 1,\n docBoost = this._documents[fieldRef.docRef].boost || 1\n\n for (var j = 0; j < termsLength; j++) {\n var term = terms[j],\n tf = termFrequencies[term],\n termIndex = this.invertedIndex[term]._index,\n idf, score, scoreWithPrecision\n\n if (termIdfCache[term] === undefined) {\n idf = lunr.idf(this.invertedIndex[term], this.documentCount)\n termIdfCache[term] = idf\n } else {\n idf = termIdfCache[term]\n }\n\n score = idf * ((this._k1 + 1) * tf) / (this._k1 * (1 - this._b + this._b * (fieldLength / this.averageFieldLength[fieldName])) + tf)\n score *= fieldBoost\n score *= docBoost\n scoreWithPrecision = Math.round(score * 1000) / 1000\n // Converts 1.23456789 to 1.234.\n // Reducing the precision so that the vectors take up less\n // space when serialised. Doing it now so that they behave\n // the same before and after serialisation. Also, this is\n // the fastest approach to reducing a number's precision in\n // JavaScript.\n\n fieldVector.insert(termIndex, scoreWithPrecision)\n }\n\n fieldVectors[fieldRef] = fieldVector\n }\n\n this.fieldVectors = fieldVectors\n}\n\n/**\n * Creates a token set of all tokens in the index using lunr.TokenSet\n *\n * @private\n */\nlunr.Builder.prototype.createTokenSet = function () {\n this.tokenSet = lunr.TokenSet.fromArray(\n Object.keys(this.invertedIndex).sort()\n )\n}\n\n/**\n * Builds the index, creating an instance of lunr.Index.\n *\n * This completes the indexing process and should only be called\n * once all documents have been added to the index.\n *\n * @returns {lunr.Index}\n */\nlunr.Builder.prototype.build = function () {\n this.calculateAverageFieldLengths()\n this.createFieldVectors()\n this.createTokenSet()\n\n return new lunr.Index({\n invertedIndex: this.invertedIndex,\n fieldVectors: this.fieldVectors,\n tokenSet: this.tokenSet,\n fields: Object.keys(this._fields),\n pipeline: this.searchPipeline\n })\n}\n\n/**\n * Applies a plugin to the index builder.\n *\n * A plugin is a function that is called with the index builder as its context.\n * Plugins can be used to customise or extend the behaviour of the index\n * in some way. A plugin is just a function, that encapsulated the custom\n * behaviour that should be applied when building the index.\n *\n * The plugin function will be called with the index builder as its argument, additional\n * arguments can also be passed when calling use. The function will be called\n * with the index builder as its context.\n *\n * @param {Function} plugin The plugin to apply.\n */\nlunr.Builder.prototype.use = function (fn) {\n var args = Array.prototype.slice.call(arguments, 1)\n args.unshift(this)\n fn.apply(this, args)\n}\n/**\n * Contains and collects metadata about a matching document.\n * A single instance of lunr.MatchData is returned as part of every\n * lunr.Index~Result.\n *\n * @constructor\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n * @property {object} metadata - A cloned collection of metadata associated with this document.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData = function (term, field, metadata) {\n var clonedMetadata = Object.create(null),\n metadataKeys = Object.keys(metadata || {})\n\n // Cloning the metadata to prevent the original\n // being mutated during match data combination.\n // Metadata is kept in an array within the inverted\n // index so cloning the data can be done with\n // Array#slice\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n clonedMetadata[key] = metadata[key].slice()\n }\n\n this.metadata = Object.create(null)\n\n if (term !== undefined) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = clonedMetadata\n }\n}\n\n/**\n * An instance of lunr.MatchData will be created for every term that matches a\n * document. However only one instance is required in a lunr.Index~Result. This\n * method combines metadata from another instance of lunr.MatchData with this\n * objects metadata.\n *\n * @param {lunr.MatchData} otherMatchData - Another instance of match data to merge with this one.\n * @see {@link lunr.Index~Result}\n */\nlunr.MatchData.prototype.combine = function (otherMatchData) {\n var terms = Object.keys(otherMatchData.metadata)\n\n for (var i = 0; i < terms.length; i++) {\n var term = terms[i],\n fields = Object.keys(otherMatchData.metadata[term])\n\n if (this.metadata[term] == undefined) {\n this.metadata[term] = Object.create(null)\n }\n\n for (var j = 0; j < fields.length; j++) {\n var field = fields[j],\n keys = Object.keys(otherMatchData.metadata[term][field])\n\n if (this.metadata[term][field] == undefined) {\n this.metadata[term][field] = Object.create(null)\n }\n\n for (var k = 0; k < keys.length; k++) {\n var key = keys[k]\n\n if (this.metadata[term][field][key] == undefined) {\n this.metadata[term][field][key] = otherMatchData.metadata[term][field][key]\n } else {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(otherMatchData.metadata[term][field][key])\n }\n\n }\n }\n }\n}\n\n/**\n * Add metadata for a term/field pair to this instance of match data.\n *\n * @param {string} term - The term this match data is associated with\n * @param {string} field - The field in which the term was found\n * @param {object} metadata - The metadata recorded about this term in this field\n */\nlunr.MatchData.prototype.add = function (term, field, metadata) {\n if (!(term in this.metadata)) {\n this.metadata[term] = Object.create(null)\n this.metadata[term][field] = metadata\n return\n }\n\n if (!(field in this.metadata[term])) {\n this.metadata[term][field] = metadata\n return\n }\n\n var metadataKeys = Object.keys(metadata)\n\n for (var i = 0; i < metadataKeys.length; i++) {\n var key = metadataKeys[i]\n\n if (key in this.metadata[term][field]) {\n this.metadata[term][field][key] = this.metadata[term][field][key].concat(metadata[key])\n } else {\n this.metadata[term][field][key] = metadata[key]\n }\n }\n}\n/**\n * A lunr.Query provides a programmatic way of defining queries to be performed\n * against a {@link lunr.Index}.\n *\n * Prefer constructing a lunr.Query using the {@link lunr.Index#query} method\n * so the query object is pre-initialized with the right index fields.\n *\n * @constructor\n * @property {lunr.Query~Clause[]} clauses - An array of query clauses.\n * @property {string[]} allFields - An array of all available fields in a lunr.Index.\n */\nlunr.Query = function (allFields) {\n this.clauses = []\n this.allFields = allFields\n}\n\n/**\n * Constants for indicating what kind of automatic wildcard insertion will be used when constructing a query clause.\n *\n * This allows wildcards to be added to the beginning and end of a term without having to manually do any string\n * concatenation.\n *\n * The wildcard constants can be bitwise combined to select both leading and trailing wildcards.\n *\n * @constant\n * @default\n * @property {number} wildcard.NONE - The term will have no wildcards inserted, this is the default behaviour\n * @property {number} wildcard.LEADING - Prepend the term with a wildcard, unless a leading wildcard already exists\n * @property {number} wildcard.TRAILING - Append a wildcard to the term, unless a trailing wildcard already exists\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with trailing wildcard\n * query.term('foo', { wildcard: lunr.Query.wildcard.TRAILING })\n * @example query term with leading and trailing wildcard\n * query.term('foo', {\n * wildcard: lunr.Query.wildcard.LEADING | lunr.Query.wildcard.TRAILING\n * })\n */\n\nlunr.Query.wildcard = new String (\"*\")\nlunr.Query.wildcard.NONE = 0\nlunr.Query.wildcard.LEADING = 1\nlunr.Query.wildcard.TRAILING = 2\n\n/**\n * Constants for indicating what kind of presence a term must have in matching documents.\n *\n * @constant\n * @enum {number}\n * @see lunr.Query~Clause\n * @see lunr.Query#clause\n * @see lunr.Query#term\n * @example query term with required presence\n * query.term('foo', { presence: lunr.Query.presence.REQUIRED })\n */\nlunr.Query.presence = {\n /**\n * Term's presence in a document is optional, this is the default value.\n */\n OPTIONAL: 1,\n\n /**\n * Term's presence in a document is required, documents that do not contain\n * this term will not be returned.\n */\n REQUIRED: 2,\n\n /**\n * Term's presence in a document is prohibited, documents that do contain\n * this term will not be returned.\n */\n PROHIBITED: 3\n}\n\n/**\n * A single clause in a {@link lunr.Query} contains a term and details on how to\n * match that term against a {@link lunr.Index}.\n *\n * @typedef {Object} lunr.Query~Clause\n * @property {string[]} fields - The fields in an index this clause should be matched against.\n * @property {number} [boost=1] - Any boost that should be applied when matching this clause.\n * @property {number} [editDistance] - Whether the term should have fuzzy matching applied, and how fuzzy the match should be.\n * @property {boolean} [usePipeline] - Whether the term should be passed through the search pipeline.\n * @property {number} [wildcard=lunr.Query.wildcard.NONE] - Whether the term should have wildcards appended or prepended.\n * @property {number} [presence=lunr.Query.presence.OPTIONAL] - The terms presence in any matching documents.\n */\n\n/**\n * Adds a {@link lunr.Query~Clause} to this query.\n *\n * Unless the clause contains the fields to be matched all fields will be matched. In addition\n * a default boost of 1 is applied to the clause.\n *\n * @param {lunr.Query~Clause} clause - The clause to add to this query.\n * @see lunr.Query~Clause\n * @returns {lunr.Query}\n */\nlunr.Query.prototype.clause = function (clause) {\n if (!('fields' in clause)) {\n clause.fields = this.allFields\n }\n\n if (!('boost' in clause)) {\n clause.boost = 1\n }\n\n if (!('usePipeline' in clause)) {\n clause.usePipeline = true\n }\n\n if (!('wildcard' in clause)) {\n clause.wildcard = lunr.Query.wildcard.NONE\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.LEADING) && (clause.term.charAt(0) != lunr.Query.wildcard)) {\n clause.term = \"*\" + clause.term\n }\n\n if ((clause.wildcard & lunr.Query.wildcard.TRAILING) && (clause.term.slice(-1) != lunr.Query.wildcard)) {\n clause.term = \"\" + clause.term + \"*\"\n }\n\n if (!('presence' in clause)) {\n clause.presence = lunr.Query.presence.OPTIONAL\n }\n\n this.clauses.push(clause)\n\n return this\n}\n\n/**\n * A negated query is one in which every clause has a presence of\n * prohibited. These queries require some special processing to return\n * the expected results.\n *\n * @returns boolean\n */\nlunr.Query.prototype.isNegated = function () {\n for (var i = 0; i < this.clauses.length; i++) {\n if (this.clauses[i].presence != lunr.Query.presence.PROHIBITED) {\n return false\n }\n }\n\n return true\n}\n\n/**\n * Adds a term to the current query, under the covers this will create a {@link lunr.Query~Clause}\n * to the list of clauses that make up this query.\n *\n * The term is used as is, i.e. no tokenization will be performed by this method. Instead conversion\n * to a token or token-like string should be done before calling this method.\n *\n * The term will be converted to a string by calling `toString`. Multiple terms can be passed as an\n * array, each term in the array will share the same options.\n *\n * @param {object|object[]} term - The term(s) to add to the query.\n * @param {object} [options] - Any additional properties to add to the query clause.\n * @returns {lunr.Query}\n * @see lunr.Query#clause\n * @see lunr.Query~Clause\n * @example adding a single term to a query\n * query.term(\"foo\")\n * @example adding a single term to a query and specifying search fields, term boost and automatic trailing wildcard\n * query.term(\"foo\", {\n * fields: [\"title\"],\n * boost: 10,\n * wildcard: lunr.Query.wildcard.TRAILING\n * })\n * @example using lunr.tokenizer to convert a string to tokens before using them as terms\n * query.term(lunr.tokenizer(\"foo bar\"))\n */\nlunr.Query.prototype.term = function (term, options) {\n if (Array.isArray(term)) {\n term.forEach(function (t) { this.term(t, lunr.utils.clone(options)) }, this)\n return this\n }\n\n var clause = options || {}\n clause.term = term.toString()\n\n this.clause(clause)\n\n return this\n}\nlunr.QueryParseError = function (message, start, end) {\n this.name = \"QueryParseError\"\n this.message = message\n this.start = start\n this.end = end\n}\n\nlunr.QueryParseError.prototype = new Error\nlunr.QueryLexer = function (str) {\n this.lexemes = []\n this.str = str\n this.length = str.length\n this.pos = 0\n this.start = 0\n this.escapeCharPositions = []\n}\n\nlunr.QueryLexer.prototype.run = function () {\n var state = lunr.QueryLexer.lexText\n\n while (state) {\n state = state(this)\n }\n}\n\nlunr.QueryLexer.prototype.sliceString = function () {\n var subSlices = [],\n sliceStart = this.start,\n sliceEnd = this.pos\n\n for (var i = 0; i < this.escapeCharPositions.length; i++) {\n sliceEnd = this.escapeCharPositions[i]\n subSlices.push(this.str.slice(sliceStart, sliceEnd))\n sliceStart = sliceEnd + 1\n }\n\n subSlices.push(this.str.slice(sliceStart, this.pos))\n this.escapeCharPositions.length = 0\n\n return subSlices.join('')\n}\n\nlunr.QueryLexer.prototype.emit = function (type) {\n this.lexemes.push({\n type: type,\n str: this.sliceString(),\n start: this.start,\n end: this.pos\n })\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.escapeCharacter = function () {\n this.escapeCharPositions.push(this.pos - 1)\n this.pos += 1\n}\n\nlunr.QueryLexer.prototype.next = function () {\n if (this.pos >= this.length) {\n return lunr.QueryLexer.EOS\n }\n\n var char = this.str.charAt(this.pos)\n this.pos += 1\n return char\n}\n\nlunr.QueryLexer.prototype.width = function () {\n return this.pos - this.start\n}\n\nlunr.QueryLexer.prototype.ignore = function () {\n if (this.start == this.pos) {\n this.pos += 1\n }\n\n this.start = this.pos\n}\n\nlunr.QueryLexer.prototype.backup = function () {\n this.pos -= 1\n}\n\nlunr.QueryLexer.prototype.acceptDigitRun = function () {\n var char, charCode\n\n do {\n char = this.next()\n charCode = char.charCodeAt(0)\n } while (charCode > 47 && charCode < 58)\n\n if (char != lunr.QueryLexer.EOS) {\n this.backup()\n }\n}\n\nlunr.QueryLexer.prototype.more = function () {\n return this.pos < this.length\n}\n\nlunr.QueryLexer.EOS = 'EOS'\nlunr.QueryLexer.FIELD = 'FIELD'\nlunr.QueryLexer.TERM = 'TERM'\nlunr.QueryLexer.EDIT_DISTANCE = 'EDIT_DISTANCE'\nlunr.QueryLexer.BOOST = 'BOOST'\nlunr.QueryLexer.PRESENCE = 'PRESENCE'\n\nlunr.QueryLexer.lexField = function (lexer) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.FIELD)\n lexer.ignore()\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexTerm = function (lexer) {\n if (lexer.width() > 1) {\n lexer.backup()\n lexer.emit(lunr.QueryLexer.TERM)\n }\n\n lexer.ignore()\n\n if (lexer.more()) {\n return lunr.QueryLexer.lexText\n }\n}\n\nlunr.QueryLexer.lexEditDistance = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.EDIT_DISTANCE)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexBoost = function (lexer) {\n lexer.ignore()\n lexer.acceptDigitRun()\n lexer.emit(lunr.QueryLexer.BOOST)\n return lunr.QueryLexer.lexText\n}\n\nlunr.QueryLexer.lexEOS = function (lexer) {\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n}\n\n// This matches the separator used when tokenising fields\n// within a document. These should match otherwise it is\n// not possible to search for some tokens within a document.\n//\n// It is possible for the user to change the separator on the\n// tokenizer so it _might_ clash with any other of the special\n// characters already used within the search string, e.g. :.\n//\n// This means that it is possible to change the separator in\n// such a way that makes some words unsearchable using a search\n// string.\nlunr.QueryLexer.termSeparator = lunr.tokenizer.separator\n\nlunr.QueryLexer.lexText = function (lexer) {\n while (true) {\n var char = lexer.next()\n\n if (char == lunr.QueryLexer.EOS) {\n return lunr.QueryLexer.lexEOS\n }\n\n // Escape character is '\\'\n if (char.charCodeAt(0) == 92) {\n lexer.escapeCharacter()\n continue\n }\n\n if (char == \":\") {\n return lunr.QueryLexer.lexField\n }\n\n if (char == \"~\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexEditDistance\n }\n\n if (char == \"^\") {\n lexer.backup()\n if (lexer.width() > 0) {\n lexer.emit(lunr.QueryLexer.TERM)\n }\n return lunr.QueryLexer.lexBoost\n }\n\n // \"+\" indicates term presence is required\n // checking for length to ensure that only\n // leading \"+\" are considered\n if (char == \"+\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n // \"-\" indicates term presence is prohibited\n // checking for length to ensure that only\n // leading \"-\" are considered\n if (char == \"-\" && lexer.width() === 1) {\n lexer.emit(lunr.QueryLexer.PRESENCE)\n return lunr.QueryLexer.lexText\n }\n\n if (char.match(lunr.QueryLexer.termSeparator)) {\n return lunr.QueryLexer.lexTerm\n }\n }\n}\n\nlunr.QueryParser = function (str, query) {\n this.lexer = new lunr.QueryLexer (str)\n this.query = query\n this.currentClause = {}\n this.lexemeIdx = 0\n}\n\nlunr.QueryParser.prototype.parse = function () {\n this.lexer.run()\n this.lexemes = this.lexer.lexemes\n\n var state = lunr.QueryParser.parseClause\n\n while (state) {\n state = state(this)\n }\n\n return this.query\n}\n\nlunr.QueryParser.prototype.peekLexeme = function () {\n return this.lexemes[this.lexemeIdx]\n}\n\nlunr.QueryParser.prototype.consumeLexeme = function () {\n var lexeme = this.peekLexeme()\n this.lexemeIdx += 1\n return lexeme\n}\n\nlunr.QueryParser.prototype.nextClause = function () {\n var completedClause = this.currentClause\n this.query.clause(completedClause)\n this.currentClause = {}\n}\n\nlunr.QueryParser.parseClause = function (parser) {\n var lexeme = parser.peekLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.type) {\n case lunr.QueryLexer.PRESENCE:\n return lunr.QueryParser.parsePresence\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expected either a field or a term, found \" + lexeme.type\n\n if (lexeme.str.length >= 1) {\n errorMessage += \" with value '\" + lexeme.str + \"'\"\n }\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n}\n\nlunr.QueryParser.parsePresence = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n switch (lexeme.str) {\n case \"-\":\n parser.currentClause.presence = lunr.Query.presence.PROHIBITED\n break\n case \"+\":\n parser.currentClause.presence = lunr.Query.presence.REQUIRED\n break\n default:\n var errorMessage = \"unrecognised presence operator'\" + lexeme.str + \"'\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term or field, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.FIELD:\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term or field, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseField = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n if (parser.query.allFields.indexOf(lexeme.str) == -1) {\n var possibleFields = parser.query.allFields.map(function (f) { return \"'\" + f + \"'\" }).join(', '),\n errorMessage = \"unrecognised field '\" + lexeme.str + \"', possible fields: \" + possibleFields\n\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.fields = [lexeme.str]\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n var errorMessage = \"expecting term, found nothing\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n return lunr.QueryParser.parseTerm\n default:\n var errorMessage = \"expecting term, found '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseTerm = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n parser.currentClause.term = lexeme.str.toLowerCase()\n\n if (lexeme.str.indexOf(\"*\") != -1) {\n parser.currentClause.usePipeline = false\n }\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseEditDistance = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var editDistance = parseInt(lexeme.str, 10)\n\n if (isNaN(editDistance)) {\n var errorMessage = \"edit distance must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.editDistance = editDistance\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\nlunr.QueryParser.parseBoost = function (parser) {\n var lexeme = parser.consumeLexeme()\n\n if (lexeme == undefined) {\n return\n }\n\n var boost = parseInt(lexeme.str, 10)\n\n if (isNaN(boost)) {\n var errorMessage = \"boost must be numeric\"\n throw new lunr.QueryParseError (errorMessage, lexeme.start, lexeme.end)\n }\n\n parser.currentClause.boost = boost\n\n var nextLexeme = parser.peekLexeme()\n\n if (nextLexeme == undefined) {\n parser.nextClause()\n return\n }\n\n switch (nextLexeme.type) {\n case lunr.QueryLexer.TERM:\n parser.nextClause()\n return lunr.QueryParser.parseTerm\n case lunr.QueryLexer.FIELD:\n parser.nextClause()\n return lunr.QueryParser.parseField\n case lunr.QueryLexer.EDIT_DISTANCE:\n return lunr.QueryParser.parseEditDistance\n case lunr.QueryLexer.BOOST:\n return lunr.QueryParser.parseBoost\n case lunr.QueryLexer.PRESENCE:\n parser.nextClause()\n return lunr.QueryParser.parsePresence\n default:\n var errorMessage = \"Unexpected lexeme type '\" + nextLexeme.type + \"'\"\n throw new lunr.QueryParseError (errorMessage, nextLexeme.start, nextLexeme.end)\n }\n}\n\n /**\n * export the module via AMD, CommonJS or as a browser global\n * Export code from https://github.com/umdjs/umd/blob/master/returnExports.js\n */\n ;(function (root, factory) {\n if (typeof define === 'function' && define.amd) {\n // AMD. Register as an anonymous module.\n define(factory)\n } else if (typeof exports === 'object') {\n /**\n * Node. Does not work with strict CommonJS, but\n * only CommonJS-like enviroments that support module.exports,\n * like Node.\n */\n module.exports = factory()\n } else {\n // Browser globals (root is window)\n root.lunr = factory()\n }\n }(this, function () {\n /**\n * Just return a value to define the module export.\n * This example returns an object, but the module\n * can return a function as the exported value.\n */\n return lunr\n }))\n})();\n","\"use strict\";\n\n// eslint-disable-next-line func-names\nmodule.exports = function () {\n if (typeof globalThis === 'object') {\n return globalThis;\n }\n\n var g;\n\n try {\n // This works if eval is allowed (see CSP)\n // eslint-disable-next-line no-new-func\n g = this || new Function('return this')();\n } catch (e) {\n // This works if the window reference is available\n if (typeof window === 'object') {\n return window;\n } // This works if the self reference is available\n\n\n if (typeof self === 'object') {\n return self;\n } // This works if the global reference is available\n\n\n if (typeof global !== 'undefined') {\n return global;\n }\n }\n\n return g;\n}();","var g;\n\n// This works in non-strict mode\ng = (function() {\n\treturn this;\n})();\n\ntry {\n\t// This works if eval is allowed (see CSP)\n\tg = g || new Function(\"return this\")();\n} catch (e) {\n\t// This works if the window reference is available\n\tif (typeof window === \"object\") g = window;\n}\n\n// g can still be undefined, but nothing to do about it...\n// We return undefined, instead of nothing here, so it's\n// easier to handle this case. if(!global) { ...}\n\nmodule.exports = g;\n","/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport var __createBinding = Object.create ? (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });\r\n}) : (function(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n});\r\n\r\nexport function __exportStar(m, o) {\r\n for (var p in m) if (p !== \"default\" && !Object.prototype.hasOwnProperty.call(o, p)) __createBinding(o, m, p);\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nvar __setModuleDefault = Object.create ? (function(o, v) {\r\n Object.defineProperty(o, \"default\", { enumerable: true, value: v });\r\n}) : function(o, v) {\r\n o[\"default\"] = v;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (k !== \"default\" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);\r\n __setModuleDefault(result, mod);\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SearchIndex, SearchResult } from \"../../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search message type\n */\nexport const enum SearchMessageType {\n SETUP, /* Search index setup */\n READY, /* Search index ready */\n QUERY, /* Search query */\n RESULT /* Search results */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * A message containing the data necessary to setup the search index\n */\nexport interface SearchSetupMessage {\n type: SearchMessageType.SETUP /* Message type */\n data: SearchIndex /* Message data */\n}\n\n/**\n * A message indicating the search index is ready\n */\nexport interface SearchReadyMessage {\n type: SearchMessageType.READY /* Message type */\n}\n\n/**\n * A message containing a search query\n */\nexport interface SearchQueryMessage {\n type: SearchMessageType.QUERY /* Message type */\n data: string /* Message data */\n}\n\n/**\n * A message containing results for a search query\n */\nexport interface SearchResultMessage {\n type: SearchMessageType.RESULT /* Message type */\n data: SearchResult[] /* Message data */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * A message exchanged with the search worker\n */\nexport type SearchMessage =\n | SearchSetupMessage\n | SearchReadyMessage\n | SearchQueryMessage\n | SearchResultMessage\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Type guard for search setup messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchSetupMessage(\n message: SearchMessage\n): message is SearchSetupMessage {\n return message.type === SearchMessageType.SETUP\n}\n\n/**\n * Type guard for search ready messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchReadyMessage(\n message: SearchMessage\n): message is SearchReadyMessage {\n return message.type === SearchMessageType.READY\n}\n\n/**\n * Type guard for search query messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchQueryMessage(\n message: SearchMessage\n): message is SearchQueryMessage {\n return message.type === SearchMessageType.QUERY\n}\n\n/**\n * Type guard for search result messages\n *\n * @param message - Search worker message\n *\n * @return Test result\n */\nexport function isSearchResultMessage(\n message: SearchMessage\n): message is SearchResultMessage {\n return message.type === SearchMessageType.RESULT\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport {\n SearchDocument,\n SearchDocumentMap,\n setupSearchDocumentMap\n} from \"../document\"\nimport {\n SearchHighlightFactoryFn,\n setupSearchHighlighter\n} from \"../highlighter\"\nimport {\n SearchQueryTerms,\n getSearchQueryTerms,\n parseSearchQuery\n} from \"../query\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index configuration\n */\nexport interface SearchIndexConfig {\n lang: string[] /* Search languages */\n separator: string /* Search separator */\n}\n\n/**\n * Search index document\n */\nexport interface SearchIndexDocument {\n location: string /* Document location */\n title: string /* Document title */\n text: string /* Document text */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search index pipeline function\n */\nexport type SearchIndexPipelineFn =\n | \"trimmer\" /* Trimmer */\n | \"stopWordFilter\" /* Stop word filter */\n | \"stemmer\" /* Stemmer */\n\n/**\n * Search index pipeline\n */\nexport type SearchIndexPipeline = SearchIndexPipelineFn[]\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search index\n *\n * This interfaces describes the format of the `search_index.json` file which\n * is automatically built by the MkDocs search plugin.\n */\nexport interface SearchIndex {\n config: SearchIndexConfig /* Search index configuration */\n docs: SearchIndexDocument[] /* Search index documents */\n index?: object /* Prebuilt index */\n pipeline?: SearchIndexPipeline /* Search index pipeline */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search metadata\n */\nexport interface SearchMetadata {\n score: number /* Score (relevance) */\n terms: SearchQueryTerms /* Search query terms */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search result\n */\nexport type SearchResult = Array<\n SearchDocument & SearchMetadata\n> // tslint:disable-line\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Compute the difference of two lists of strings\n *\n * @param a - 1st list of strings\n * @param b - 2nd list of strings\n *\n * @return Difference\n */\nfunction difference(a: string[], b: string[]): string[] {\n const [x, y] = [new Set(a), new Set(b)]\n return [\n ...new Set([...x].filter(value => !y.has(value)))\n ]\n}\n\n/* ----------------------------------------------------------------------------\n * Class\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n *\n * Note that `lunr` is injected via Webpack, as it will otherwise also be\n * bundled in the application bundle.\n */\nexport class Search {\n\n /**\n * Search document mapping\n *\n * A mapping of URLs (including hash fragments) to the actual articles and\n * sections of the documentation. The search document mapping must be created\n * regardless of whether the index was prebuilt or not, as `lunr` itself will\n * only store the actual index.\n */\n protected documents: SearchDocumentMap\n\n /**\n * Search highlight factory function\n */\n protected highlight: SearchHighlightFactoryFn\n\n /**\n * The underlying `lunr` search index\n */\n protected index: lunr.Index\n\n /**\n * Create the search integration\n *\n * @param data - Search index\n */\n public constructor({ config, docs, pipeline, index }: SearchIndex) {\n this.documents = setupSearchDocumentMap(docs)\n this.highlight = setupSearchHighlighter(config)\n\n /* Set separator for tokenizer */\n lunr.tokenizer.separator = new RegExp(config.separator)\n\n /* If no index was given, create it */\n if (typeof index === \"undefined\") {\n this.index = lunr(function() {\n\n /* Set up multi-language support */\n if (config.lang.length === 1 && config.lang[0] !== \"en\") {\n this.use((lunr as any)[config.lang[0]])\n } else if (config.lang.length > 1) {\n this.use((lunr as any).multiLanguage(...config.lang))\n }\n\n /* Compute functions to be removed from the pipeline */\n const fns = difference([\n \"trimmer\", \"stopWordFilter\", \"stemmer\"\n ], pipeline!)\n\n /* Remove functions from the pipeline for registered languages */\n for (const lang of config.lang.map(language => (\n language === \"en\" ? lunr : (lunr as any)[language]\n ))) {\n for (const fn of fns) {\n this.pipeline.remove(lang[fn])\n this.searchPipeline.remove(lang[fn])\n }\n }\n\n /* Set up fields and reference */\n this.field(\"title\", { boost: 1000 })\n this.field(\"text\")\n this.ref(\"location\")\n\n /* Index documents */\n for (const doc of docs)\n this.add(doc)\n })\n\n /* Handle prebuilt index */\n } else {\n this.index = lunr.Index.load(index)\n }\n }\n\n /**\n * Search for matching documents\n *\n * The search index which MkDocs provides is divided up into articles, which\n * contain the whole content of the individual pages, and sections, which only\n * contain the contents of the subsections obtained by breaking the individual\n * pages up at `h1` ... `h6`. As there may be many sections on different pages\n * with identical titles (for example within this very project, e.g. \"Usage\"\n * or \"Installation\"), they need to be put into the context of the containing\n * page. For this reason, section results are grouped within their respective\n * articles which are the top-level results that are returned.\n *\n * @param query - Query value\n *\n * @return Search results\n */\n public search(query: string): SearchResult[] {\n if (query) {\n try {\n const highlight = this.highlight(query)\n\n /* Parse query to extract clauses for analysis */\n const clauses = parseSearchQuery(query)\n .filter(clause => (\n clause.presence !== lunr.Query.presence.PROHIBITED\n ))\n\n /* Perform search and post-process results */\n const groups = this.index.search(`${query}*`)\n\n /* Apply post-query boosts based on title and search query terms */\n .reduce((results, { ref, score, matchData }) => {\n const document = this.documents.get(ref)\n if (typeof document !== \"undefined\") {\n const { location, title, text, parent } = document\n\n /* Compute and analyze search query terms */\n const terms = getSearchQueryTerms(\n clauses,\n Object.keys(matchData.metadata)\n )\n\n /* Highlight title and text and apply post-query boosts */\n const boost = +!parent + +Object.values(terms).every(t => t)\n results.push({\n location,\n title: highlight(title),\n text: highlight(text),\n score: score * (1 + boost),\n terms\n })\n }\n return results\n }, [])\n\n /* Sort search results again after applying boosts */\n .sort((a, b) => b.score - a.score)\n\n /* Group search results by page */\n .reduce((results, result) => {\n const document = this.documents.get(result.location)\n if (typeof document !== \"undefined\") {\n const ref = \"parent\" in document\n ? document.parent!.location\n : document.location\n results.set(ref, [...results.get(ref) || [], result])\n }\n return results\n }, new Map())\n\n /* Expand grouped search results */\n return [...groups.values()]\n\n /* Log errors to console (for now) */\n } catch {\n // tslint:disable-next-line no-console\n console.warn(`Invalid query: ${query} – see https://bit.ly/2s3ChXG`)\n }\n }\n\n /* Return nothing in case of error or empty query */\n return []\n }\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n// @ts-ignore\nimport * as escapeHTML from \"escape-html\"\n\nimport { SearchIndexDocument } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search document\n */\nexport interface SearchDocument extends SearchIndexDocument {\n parent?: SearchIndexDocument /* Parent article */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search document mapping\n */\nexport type SearchDocumentMap = Map\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search document mapping\n *\n * @param docs - Search index documents\n *\n * @return Search document map\n */\nexport function setupSearchDocumentMap(\n docs: SearchIndexDocument[]\n): SearchDocumentMap {\n const documents = new Map()\n const parents = new Set()\n for (const doc of docs) {\n const [path, hash] = doc.location.split(\"#\")\n\n /* Extract location and title */\n const location = doc.location\n const title = doc.title\n\n /* Escape and cleanup text */\n const text = escapeHTML(doc.text)\n .replace(/\\s+(?=[,.:;!?])/g, \"\")\n .replace(/\\s+/g, \" \")\n\n /* Handle section */\n if (hash) {\n const parent = documents.get(path)!\n\n /* Ignore first section, override article */\n if (!parents.has(parent)) {\n parent.title = doc.title\n parent.text = text\n\n /* Remember that we processed the article */\n parents.add(parent)\n\n /* Add subsequent section */\n } else {\n documents.set(location, {\n location,\n title,\n text,\n parent\n })\n }\n\n /* Add article */\n } else {\n documents.set(location, {\n location,\n title,\n text\n })\n }\n }\n return documents\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport { SearchIndexConfig } from \"../_\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search highlight function\n *\n * @param value - Value\n *\n * @return Highlighted value\n */\nexport type SearchHighlightFn = (value: string) => string\n\n/**\n * Search highlight factory function\n *\n * @param query - Query value\n *\n * @return Search highlight function\n */\nexport type SearchHighlightFactoryFn = (query: string) => SearchHighlightFn\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Create a search highlighter\n *\n * @param config - Search index configuration\n *\n * @return Search highlight factory function\n */\nexport function setupSearchHighlighter(\n config: SearchIndexConfig\n): SearchHighlightFactoryFn {\n const separator = new RegExp(config.separator, \"img\")\n const highlight = (_: unknown, data: string, term: string) => {\n return `${data}${term}`\n }\n\n /* Return factory function */\n return (query: string) => {\n query = query\n .replace(/[\\s*+\\-:~^]+/g, \" \")\n .trim()\n\n /* Create search term match expression */\n const match = new RegExp(`(^|${config.separator})(${\n query\n .replace(/[|\\\\{}()[\\]^$+*?.-]/g, \"\\\\$&\")\n .replace(separator, \"|\")\n })`, \"img\")\n\n /* Highlight string value */\n return value => value\n .replace(match, highlight)\n .replace(/<\\/mark>(\\s+)]*>/img, \"\\$1\")\n }\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Search query clause\n */\nexport interface SearchQueryClause {\n presence: lunr.Query.presence /* Clause presence */\n term: string /* Clause term */\n}\n\n/* ------------------------------------------------------------------------- */\n\n/**\n * Search query terms\n */\nexport type SearchQueryTerms = Record\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Parse a search query for analysis\n *\n * @param value - Query value\n *\n * @return Search query clauses\n */\nexport function parseSearchQuery(\n value: string\n): SearchQueryClause[] {\n const query = new (lunr as any).Query([\"title\", \"text\"])\n const parser = new (lunr as any).QueryParser(value, query)\n\n /* Parse and return query clauses */\n parser.parse()\n return query.clauses\n}\n\n/**\n * Analyze the search query clauses in regard to the search terms found\n *\n * @param query - Search query clauses\n * @param terms - Search terms\n *\n * @return Search query terms\n */\nexport function getSearchQueryTerms(\n query: SearchQueryClause[], terms: string[]\n): SearchQueryTerms {\n const clauses = new Set(query)\n\n /* Match query clauses against terms */\n const result: SearchQueryTerms = {}\n for (let t = 0; t < terms.length; t++)\n for (const clause of clauses)\n if (terms[t].startsWith(clause.term)) {\n result[clause.term] = true\n clauses.delete(clause)\n }\n\n /* Annotate unmatched query clauses */\n for (const clause of clauses)\n result[clause.term] = false\n\n /* Return query terms */\n return result\n}\n","/*\n * Copyright (c) 2016-2020 Martin Donath \n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to\n * deal in the Software without restriction, including without limitation the\n * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or\n * sell copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in\n * all copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A RTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS\n * IN THE SOFTWARE.\n */\n\nimport \"lunr\"\n\nimport { Search, SearchIndexConfig } from \"../../_\"\nimport {\n SearchMessage,\n SearchMessageType\n} from \"../message\"\n\n/* ----------------------------------------------------------------------------\n * Types\n * ------------------------------------------------------------------------- */\n\n/**\n * Add support for usage with `iframe-worker` polyfill\n *\n * While `importScripts` is synchronous when executed inside of a web worker,\n * it's not possible to provide a synchronous polyfilled implementation. The\n * cool thing is that awaiting a non-Promise is a noop, so extending the type\n * definition to return a `Promise` shouldn't break anything.\n *\n * @see https://bit.ly/2PjDnXi - GitHub comment\n */\ndeclare global {\n function importScripts(...urls: string[]): Promise | void\n}\n\n/* ----------------------------------------------------------------------------\n * Data\n * ------------------------------------------------------------------------- */\n\n/**\n * Search index\n */\nlet index: Search\n\n/* ----------------------------------------------------------------------------\n * Helper functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Fetch (= import) multi-language support through `lunr-languages`\n *\n * This function will automatically import the stemmers necessary to process\n * the languages which were given through the search index configuration.\n *\n * If the worker runs inside of an `iframe` (when using `iframe-worker` as\n * a shim), the base URL for the stemmers to be loaded must be determined by\n * searching for the first `script` element with a `src` attribute, which will\n * contain the contents of this script.\n *\n * @param config - Search index configuration\n *\n * @return Promise resolving with no result\n */\nasync function setupSearchLanguages(\n config: SearchIndexConfig\n): Promise {\n let base = \"../lunr\"\n\n /* Detect `iframe-worker` and fix base URL */\n if (typeof parent !== \"undefined\" && \"IFrameWorker\" in parent) {\n const worker = document.querySelector(\"script[src]\")!\n const [path] = worker.src.split(\"/worker\")\n\n /* Prefix base with path */\n base = base.replace(\"..\", path)\n }\n\n /* Add scripts for languages */\n const scripts = []\n for (const lang of config.lang) {\n if (lang === \"ja\") scripts.push(`${base}/tinyseg.min.js`)\n if (lang !== \"en\") scripts.push(`${base}/min/lunr.${lang}.min.js`)\n }\n\n /* Add multi-language support */\n if (config.lang.length > 1)\n scripts.push(`${base}/min/lunr.multi.min.js`)\n\n /* Load scripts synchronously */\n if (scripts.length)\n await importScripts(\n `${base}/min/lunr.stemmer.support.min.js`,\n ...scripts\n )\n}\n\n/* ----------------------------------------------------------------------------\n * Functions\n * ------------------------------------------------------------------------- */\n\n/**\n * Message handler\n *\n * @param message - Source message\n *\n * @return Target message\n */\nexport async function handler(\n message: SearchMessage\n): Promise {\n switch (message.type) {\n\n /* Search setup message */\n case SearchMessageType.SETUP:\n await setupSearchLanguages(message.data.config)\n index = new Search(message.data)\n return {\n type: SearchMessageType.READY\n }\n\n /* Search query message */\n case SearchMessageType.QUERY:\n return {\n type: SearchMessageType.RESULT,\n data: index ? index.search(message.data) : []\n }\n\n /* All other messages */\n default:\n throw new TypeError(\"Invalid message type\")\n }\n}\n\n/* ----------------------------------------------------------------------------\n * Worker\n * ------------------------------------------------------------------------- */\n\naddEventListener(\"message\", async ev => {\n postMessage(await handler(ev.data))\n})\n"],"sourceRoot":""} \ No newline at end of file diff --git a/v2/assets/stylesheets/main.38780c08.min.css b/v2/assets/stylesheets/main.38780c08.min.css new file mode 100644 index 000000000..51d1aaf9d --- /dev/null +++ b/v2/assets/stylesheets/main.38780c08.min.css @@ -0,0 +1,3 @@ +html{box-sizing:border-box;-webkit-text-size-adjust:none;-moz-text-size-adjust:none;-ms-text-size-adjust:none;text-size-adjust:none}*,*::before,*::after{box-sizing:inherit}body{margin:0}hr{box-sizing:content-box;overflow:visible}a,button,label,input{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:separate;border-spacing:0}td,th{font-weight:normal;vertical-align:top}button{margin:0;padding:0;font-size:inherit;background:transparent;border:0}input{border:0;outline:none}:root{--md-default-fg-color: hsla(0, 0%, 0%, 0.87);--md-default-fg-color--light: hsla(0, 0%, 0%, 0.54);--md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32);--md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07);--md-default-bg-color: hsla(0, 0%, 100%, 1);--md-default-bg-color--light: hsla(0, 0%, 100%, 0.7);--md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3);--md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12);--md-primary-fg-color: hsla(231, 48%, 48%, 1);--md-primary-fg-color--light: hsla(230, 44%, 64%, 1);--md-primary-fg-color--dark: hsla(232, 54%, 41%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);--md-accent-fg-color: hsla(231, 99%, 66%, 1);--md-accent-fg-color--transparent: hsla(231, 99%, 66%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}:root>*{--md-code-fg-color: hsla(200, 18%, 26%, 1);--md-code-bg-color: hsla(0, 0%, 96%, 1);--md-code-hl-color: hsla(60, 100%, 50%, 0.5);--md-code-hl-number-color: hsla(0, 67%, 50%, 1);--md-code-hl-special-color: hsla(340, 83%, 47%, 1);--md-code-hl-function-color: hsla(291, 45%, 50%, 1);--md-code-hl-constant-color: hsla(250, 63%, 60%, 1);--md-code-hl-keyword-color: hsla(219, 54%, 51%, 1);--md-code-hl-string-color: hsla(150, 63%, 30%, 1);--md-code-hl-name-color: var(--md-code-fg-color);--md-code-hl-operator-color: var(--md-default-fg-color--light);--md-code-hl-punctuation-color: var(--md-default-fg-color--light);--md-code-hl-comment-color: var(--md-default-fg-color--light);--md-code-hl-generic-color: var(--md-default-fg-color--light);--md-code-hl-variable-color: var(--md-default-fg-color--light);--md-typeset-color: var(--md-default-fg-color);--md-typeset-a-color: var(--md-primary-fg-color);--md-typeset-mark-color: hsla(60, 100%, 50%, 0.5);--md-typeset-del-color: hsla(6, 90%, 60%, 0.15);--md-typeset-ins-color: hsla(150, 90%, 44%, 0.15);--md-typeset-kbd-color: hsla(0, 0%, 98%, 1);--md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1);--md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1);--md-admonition-fg-color: var(--md-default-fg-color);--md-admonition-bg-color: var(--md-default-bg-color);--md-footer-fg-color: hsla(0, 0%, 100%, 1);--md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7);--md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.3);--md-footer-bg-color: hsla(0, 0%, 0%, 0.87);--md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32)}.md-icon svg{display:block;width:1.2rem;height:1.2rem;margin:0 auto;fill:currentColor}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,input{color:var(--md-typeset-color);font-feature-settings:"kern","liga";font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif}code,pre,kbd{color:var(--md-typeset-color);font-feature-settings:"kern";font-family:SFMono-Regular,Consolas,Menlo,monospace}:root{--md-typeset-table--ascending: url('data:image/svg+xml;charset=utf-8,');--md-typeset-table--descending: url('data:image/svg+xml;charset=utf-8,')}.md-typeset{font-size:.8rem;line-height:1.6;-webkit-print-color-adjust:exact;color-adjust:exact}@media print{.md-typeset{font-size:.68rem}}.md-typeset p,.md-typeset ul,.md-typeset ol,.md-typeset blockquote{margin:1em 0}.md-typeset h1{margin:0 0 1.25em;color:var(--md-default-fg-color--light);font-weight:300;font-size:2em;line-height:1.3;letter-spacing:-0.01em}.md-typeset h2{margin:1.6em 0 .64em;font-weight:300;font-size:1.5625em;line-height:1.4;letter-spacing:-0.01em}.md-typeset h3{margin:1.6em 0 .8em;font-weight:400;font-size:1.25em;line-height:1.5;letter-spacing:-0.01em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{margin:1em 0;font-weight:700;letter-spacing:-0.01em}.md-typeset h5,.md-typeset h6{margin:1.25em 0;color:var(--md-default-fg-color--light);font-weight:700;font-size:.8em;letter-spacing:-0.01em}.md-typeset h5{text-transform:uppercase}.md-typeset hr{margin:1.5em 0;border-bottom:.05rem dotted var(--md-default-fg-color--lighter)}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a::before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset code,.md-typeset pre,.md-typeset kbd{color:var(--md-code-fg-color);direction:ltr}@media print{.md-typeset code,.md-typeset pre,.md-typeset kbd{white-space:pre-wrap}}.md-typeset code{padding:0 .2941176471em;font-size:.85em;word-break:break-word;background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset h1 code,.md-typeset h2 code,.md-typeset h3 code,.md-typeset h4 code,.md-typeset h5 code,.md-typeset h6 code{margin:initial;padding:initial;background-color:transparent;box-shadow:none}.md-typeset a>code{color:currentColor}.md-typeset pre{position:relative;margin:1em 0;line-height:1.4}.md-typeset pre>code{display:block;margin:0;padding:.7720588235em 1.1764705882em;overflow:auto;word-break:normal;box-shadow:none;-webkit-box-decoration-break:slice;box-decoration-break:slice;touch-action:auto;scrollbar-width:thin;scrollbar-color:var(--md-default-fg-color--lighter) transparent}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-typeset pre>code::-webkit-scrollbar{width:.2rem;height:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@media screen and (max-width: 44.9375em){.md-typeset>pre{margin:1em -0.8rem}.md-typeset>pre code{border-radius:0}}.md-typeset kbd{display:inline-block;padding:0 .6666666667em;color:var(--md-default-fg-color);font-size:.75em;vertical-align:text-top;word-break:break-word;background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -0.1rem .2rem var(--md-typeset-kbd-accent-color) inset}.md-typeset mark{color:inherit;word-break:break-word;background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset abbr{text-decoration:none;border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help}@media(hover: none){.md-typeset abbr{position:relative}.md-typeset abbr[title]:focus::after,.md-typeset abbr[title]:hover::after{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);position:absolute;left:0;display:inline-block;width:auto;min-width:-webkit-max-content;min-width:-moz-max-content;min-width:max-content;max-width:80%;margin-top:2em;padding:.2rem .3rem;color:var(--md-default-bg-color);font-size:.7rem;background:var(--md-default-fg-color);border-radius:.1rem;content:attr(title)}}.md-typeset small{opacity:.75}.md-typeset sup,.md-typeset sub{margin-left:.078125em}[dir=rtl] .md-typeset sup,[dir=rtl] .md-typeset sub{margin-right:.078125em;margin-left:initial}.md-typeset blockquote{padding-left:.6rem;color:var(--md-default-fg-color--light);border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{padding-right:.6rem;padding-left:initial;border-right:.2rem solid var(--md-default-fg-color--lighter);border-left:initial}.md-typeset ul{list-style-type:disc}.md-typeset ul,.md-typeset ol{margin-left:.625em;padding:0}[dir=rtl] .md-typeset ul,[dir=rtl] .md-typeset ol{margin-right:.625em;margin-left:initial}.md-typeset ul ol,.md-typeset ol ol{list-style-type:lower-alpha}.md-typeset ul ol ol,.md-typeset ol ol ol{list-style-type:lower-roman}.md-typeset ul li,.md-typeset ol li{margin-bottom:.5em;margin-left:1.25em}[dir=rtl] .md-typeset ul li,[dir=rtl] .md-typeset ol li{margin-right:1.25em;margin-left:initial}.md-typeset ul li p,.md-typeset ul li blockquote,.md-typeset ol li p,.md-typeset ol li blockquote{margin:.5em 0}.md-typeset ul li:last-child,.md-typeset ol li:last-child{margin-bottom:0}.md-typeset ul li ul,.md-typeset ul li ol,.md-typeset ol li ul,.md-typeset ol li ol{margin:.5em 0 .5em .625em}[dir=rtl] .md-typeset ul li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ol li ol{margin-right:.625em;margin-left:initial}.md-typeset dd{margin:1em 0 1.5em 1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em;margin-left:initial}.md-typeset img,.md-typeset svg{max-width:100%;height:auto}.md-typeset img[align=left],.md-typeset svg[align=left]{margin:1em;margin-left:0}.md-typeset img[align=right],.md-typeset svg[align=right]{margin:1em;margin-right:0}.md-typeset img[align]:only-child,.md-typeset svg[align]:only-child{margin-top:0}.md-typeset figure{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content;max-width:100%;margin:0 auto;text-align:center}.md-typeset figcaption{max-width:24rem;margin:.5em auto 2em;font-style:italic}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){display:inline-block;max-width:100%;overflow:auto;font-size:.64rem;background:var(--md-default-bg-color);border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1);touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) th>*:first-child,.md-typeset table:not([class]) td>*:first-child{margin-top:0}.md-typeset table:not([class]) th>*:last-child,.md-typeset table:not([class]) td>*:last-child{margin-bottom:0}.md-typeset table:not([class]) th:not([align]),.md-typeset table:not([class]) td:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) th:not([align]),[dir=rtl] .md-typeset table:not([class]) td:not([align]){text-align:right}.md-typeset table:not([class]) th{min-width:5rem;padding:.9375em 1.25em;color:var(--md-default-bg-color);vertical-align:top;background-color:var(--md-default-fg-color--light)}.md-typeset table:not([class]) th a{color:inherit}.md-typeset table:not([class]) td{padding:.9375em 1.25em;vertical-align:top;border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-typeset table:not([class]) tr{transition:background-color 125ms}.md-typeset table:not([class]) tr:hover{background-color:rgba(0,0,0,.035);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) tr:first-child td{border-top:0}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}.md-typeset table th[role=columnheader]::after{display:inline-block;width:1.2em;height:1.2em;margin-left:.5em;vertical-align:sub;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:" "}.md-typeset table th[role=columnheader][aria-sort=ascending]::after{background-color:currentColor;-webkit-mask-image:var(--md-typeset-table--ascending);mask-image:var(--md-typeset-table--ascending)}.md-typeset table th[role=columnheader][aria-sort=descending]::after{background-color:currentColor;-webkit-mask-image:var(--md-typeset-table--descending);mask-image:var(--md-typeset-table--descending)}.md-typeset__scrollwrap{margin:1em -0.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;width:100%;margin:0;overflow:hidden}html{height:100%;overflow-x:hidden;font-size:125%}@media screen and (min-width: 100em){html{font-size:137.5%}}@media screen and (min-width: 125em){html{font-size:150%}}body{position:relative;display:flex;flex-direction:column;width:100%;min-height:100%;font-size:.5rem;background-color:var(--md-default-bg-color)}@media screen and (max-width: 59.9375em){body[data-md-state=lock]{position:fixed}}@media print{body{display:block}}hr{display:block;height:.05rem;padding:0;border:0}.md-grid{max-width:61rem;margin-right:auto;margin-left:auto}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.md-toggle{display:none}.md-overlay{position:fixed;top:0;z-index:3;width:0;height:0;background-color:rgba(0,0,0,.54);opacity:0;transition:width 0ms 250ms,height 0ms 250ms,opacity 250ms}@media screen and (max-width: 76.1875em){[data-md-toggle=drawer]:checked~.md-overlay{width:100%;height:100%;opacity:1;transition:width 0ms,height 0ms,opacity 250ms}}.md-skip{position:fixed;z-index:-1;margin:.5rem;padding:.3rem .5rem;color:var(--md-default-bg-color);font-size:.64rem;background-color:var(--md-default-fg-color);border-radius:.1rem;transform:translateY(0.4rem);opacity:0}.md-skip:focus{z-index:10;transform:translateY(0);opacity:1;transition:transform 250ms cubic-bezier(0.4, 0, 0.2, 1),opacity 175ms 75ms}@page{margin:25mm}.md-announce{overflow:auto;background-color:var(--md-footer-bg-color)}.md-announce__inner{margin:.6rem auto;padding:0 .8rem;color:var(--md-footer-fg-color);font-size:.7rem}@media print{.md-announce{display:none}}.md-typeset .md-button{display:inline-block;padding:.625em 2em;color:var(--md-primary-fg-color);font-weight:700;border:.1rem solid currentColor;border-radius:.1rem;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{color:var(--md-accent-bg-color);background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color)}:root{--md-clipboard-icon: url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{position:absolute;top:.5em;right:.5em;z-index:1;width:1.5em;height:1.5em;color:var(--md-default-fg-color--lightest);border-radius:.1rem;cursor:pointer;transition:color 125ms}@media print{.md-clipboard{display:none}}.md-clipboard::after{display:block;width:1.125em;height:1.125em;margin:0 auto;background-color:currentColor;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}pre:hover .md-clipboard{color:var(--md-default-fg-color--light)}pre .md-clipboard:focus,pre .md-clipboard:hover{color:var(--md-accent-fg-color)}.md-content{flex:1;max-width:100%}@media screen and (min-width: 60em)and (max-width: 76.1875em){.md-content{max-width:calc(100% - 12.1rem)}}@media screen and (min-width: 76.25em){.md-content{max-width:calc(100% - 12.1rem * 2)}}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width: 76.25em){.md-content__inner{margin-right:1.2rem;margin-left:1.2rem}}.md-content__inner::before{display:block;height:.4rem;content:""}.md-content__inner>:last-child{margin-bottom:0}.md-content__button{float:right;margin:.4rem 0;margin-left:.4rem;padding:0}[dir=rtl] .md-content__button{float:left;margin-right:.4rem;margin-left:initial}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}@media print{.md-content__button{display:none}}.md-dialog{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);position:fixed;right:.8rem;bottom:.8rem;left:initial;z-index:2;display:block;min-width:11.1rem;padding:.4rem .6rem;color:var(--md-default-bg-color);font-size:.7rem;background:var(--md-default-fg-color);border:none;border-radius:.1rem;transform:translateY(100%);opacity:0;transition:transform 0ms 400ms,opacity 400ms}[dir=rtl] .md-dialog{right:initial;left:.8rem}.md-dialog[data-md-state=open]{transform:translateY(0);opacity:1;transition:transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1),opacity 400ms}@media print{.md-dialog{display:none}}.md-header{position:-webkit-sticky;position:sticky;top:0;right:0;left:0;z-index:2;height:2.4rem;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem rgba(0,0,0,0),0 .2rem .4rem rgba(0,0,0,0);transition:color 250ms,background-color 250ms}.no-js .md-header{box-shadow:none;transition:none}.md-header[data-md-state=shadow]{box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);transition:color 250ms,background-color 250ms,box-shadow 250ms}@media print{.md-header{display:none}}.md-header-nav{display:flex;padding:0 .2rem}.md-header-nav__button{position:relative;z-index:1;display:block;margin:.2rem;padding:.4rem;cursor:pointer;transition:opacity 250ms}[dir=rtl] .md-header-nav__button svg{transform:scaleX(-1)}.md-header-nav__button:focus,.md-header-nav__button:hover{opacity:.7}.md-header-nav__button.md-logo{margin:.2rem;padding:.4rem}.md-header-nav__button.md-logo img,.md-header-nav__button.md-logo svg{display:block;width:1.2rem;height:1.2rem;fill:currentColor}.no-js .md-header-nav__button[for=__search]{display:none}@media screen and (min-width: 60em){.md-header-nav__button[for=__search]{display:none}}@media screen and (max-width: 76.1875em){.md-header-nav__button.md-logo{display:none}}@media screen and (min-width: 76.25em){.md-header-nav__button[for=__drawer]{display:none}}.md-header-nav__topic{position:absolute;width:100%;transition:transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms}.md-header-nav__topic+.md-header-nav__topic{z-index:-1;transform:translateX(1.25rem);opacity:0;transition:transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),opacity 150ms;pointer-events:none}[dir=rtl] .md-header-nav__topic+.md-header-nav__topic{transform:translateX(-1.25rem)}.no-js .md-header-nav__topic{position:initial}.no-js .md-header-nav__topic+.md-header-nav__topic{display:none}.md-header-nav__title{flex-grow:1;padding:0 1rem;font-size:.9rem;line-height:2.4rem}.md-header-nav__title[data-md-state=active] .md-header-nav__topic{z-index:-1;transform:translateX(-1.25rem);opacity:0;transition:transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),opacity 150ms;pointer-events:none}[dir=rtl] .md-header-nav__title[data-md-state=active] .md-header-nav__topic{transform:translateX(1.25rem)}.md-header-nav__title[data-md-state=active] .md-header-nav__topic+.md-header-nav__topic{z-index:0;transform:translateX(0);opacity:1;transition:transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms;pointer-events:initial}.md-header-nav__title>.md-header-nav__ellipsis{position:relative;width:100%;height:100%}.md-header-nav__source{display:none}@media screen and (min-width: 60em){.md-header-nav__source{display:block;width:11.7rem;max-width:11.7rem;margin-left:1rem}[dir=rtl] .md-header-nav__source{margin-right:1rem;margin-left:initial}}@media screen and (min-width: 76.25em){.md-header-nav__source{margin-left:1.4rem}[dir=rtl] .md-header-nav__source{margin-right:1.4rem}}.md-footer{color:var(--md-footer-fg-color);background-color:var(--md-footer-bg-color)}@media print{.md-footer{display:none}}.md-footer-nav__inner{padding:.2rem;overflow:auto}.md-footer-nav__link{display:flex;padding-top:1.4rem;padding-bottom:.4rem;transition:opacity 250ms}@media screen and (min-width: 45em){.md-footer-nav__link{width:50%}}.md-footer-nav__link:focus,.md-footer-nav__link:hover{opacity:.7}.md-footer-nav__link--prev{float:left}[dir=rtl] .md-footer-nav__link--prev{float:right}[dir=rtl] .md-footer-nav__link--prev svg{transform:scaleX(-1)}@media screen and (max-width: 44.9375em){.md-footer-nav__link--prev{width:25%}.md-footer-nav__link--prev .md-footer-nav__title{display:none}}.md-footer-nav__link--next{float:right;text-align:right}[dir=rtl] .md-footer-nav__link--next{float:left;text-align:left}[dir=rtl] .md-footer-nav__link--next svg{transform:scaleX(-1)}@media screen and (max-width: 44.9375em){.md-footer-nav__link--next{width:75%}}.md-footer-nav__title{position:relative;flex-grow:1;max-width:calc(100% - 2.4rem);padding:0 1rem;font-size:.9rem;line-height:2.4rem}.md-footer-nav__button{margin:.2rem;padding:.4rem}.md-footer-nav__direction{position:absolute;right:0;left:0;margin-top:-1rem;padding:0 1rem;font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-footer-copyright{width:100%;margin:auto .6rem;padding:.4rem 0;color:var(--md-footer-fg-color--lighter);font-size:.64rem}@media screen and (min-width: 45em){.md-footer-copyright{width:auto}}.md-footer-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-footer-social{margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width: 45em){.md-footer-social{padding:.6rem 0}}.md-footer-social__link{display:inline-block;width:1.6rem;height:1.6rem;text-align:center}.md-footer-social__link::before{line-height:1.9}.md-footer-social__link svg{max-height:.8rem;vertical-align:-25%;fill:currentColor}:root{--md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next: url('data:image/svg+xml;charset=utf-8,');--md-toc-icon: url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{display:block;padding:0 .6rem;overflow:hidden;font-weight:700;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{width:100%;height:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{display:block;width:2.4rem;height:2.4rem}.md-nav__title .md-nav__button.md-logo svg{fill:currentColor}.md-nav__list{margin:0;padding:0;list-style:none}.md-nav__item{padding:0 .6rem}.md-nav__item:last-child{padding-bottom:.6rem}.md-nav__item .md-nav__item{padding-right:0}[dir=rtl] .md-nav__item .md-nav__item{padding-right:.6rem;padding-left:0}.md-nav__item .md-nav__item:last-child{padding-bottom:0}.md-nav__link{display:block;margin-top:.625em;overflow:hidden;text-overflow:ellipsis;cursor:pointer;transition:color 125ms;scroll-snap-align:start}html .md-nav__link[for=__toc]{display:none}html .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__link[data-md-state=blur]{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{color:var(--md-typeset-a-color)}.md-nav__item--nested>.md-nav__link{color:inherit}.md-nav__link:focus,.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav__source{display:none}@media screen and (max-width: 76.1875em){.md-nav{background-color:var(--md-default-bg-color)}.md-nav--primary,.md-nav--primary .md-nav{position:absolute;top:0;right:0;left:0;z-index:1;display:flex;flex-direction:column;height:100%}.md-nav--primary .md-nav__title,.md-nav--primary .md-nav__item{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{position:relative;height:5.6rem;padding:3rem .8rem .2rem;color:var(--md-default-fg-color--light);font-weight:400;line-height:2.4rem;white-space:nowrap;background-color:var(--md-default-fg-color--lightest);cursor:pointer}.md-nav--primary .md-nav__title .md-nav__icon{position:absolute;top:.4rem;left:.4rem;display:block;width:1.2rem;height:1.2rem;margin:.2rem}.md-nav--primary .md-nav__title .md-nav__icon::after{display:block;width:100%;height:100%;background-color:currentColor;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem;left:initial}.md-nav--primary .md-nav__title~.md-nav__list{overflow-y:auto;background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;-webkit-scroll-snap-type:y mandatory;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>.md-nav__item:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{position:relative;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color)}.md-nav--primary .md-nav__title[for=__drawer] .md-nav__button{position:absolute;top:.2rem;left:.2rem;display:block;margin:.2rem;padding:.4rem;font-size:2.4rem}html [dir=rtl] .md-nav--primary .md-nav__title[for=__drawer] .md-nav__button{right:.2rem;left:initial}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{padding:0;border-top:.05rem solid var(--md-default-fg-color--lightest)}[dir=rtl] .md-nav--primary .md-nav__item{padding:0}.md-nav--primary .md-nav__item--nested>.md-nav__link{padding-right:2.4rem}[dir=rtl] .md-nav--primary .md-nav__item--nested>.md-nav__link{padding-right:.8rem;padding-left:2.4rem}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{position:relative;margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link .md-nav__icon{position:absolute;top:50%;right:.6rem;width:1.2rem;height:1.2rem;margin-top:-0.6rem;color:inherit;font-size:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon::after{display:block;width:100%;height:100%;background-color:currentColor;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{right:initial;left:.6rem}[dir=rtl] .md-nav--primary .md-nav__icon::after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav__link{position:static}.md-nav--primary .md-nav--secondary .md-nav{position:static;background-color:transparent}.md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem;padding-left:initial}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem;padding-left:initial}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem;padding-left:initial}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem;padding-left:initial}.md-nav__toggle~.md-nav{display:flex;transform:translateX(100%);opacity:0;transition:transform 250ms cubic-bezier(0.8, 0, 0.6, 1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{transform:translateX(0);opacity:1;transition:transform 250ms cubic-bezier(0.4, 0, 0.2, 1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width: 59.9375em){html .md-nav__link[for=__toc]{display:block;padding-right:2.4rem}html .md-nav__link[for=__toc]+.md-nav__link{display:none}html .md-nav__link[for=__toc] .md-icon::after{display:block;width:100%;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);background-color:currentColor;content:""}html .md-nav__link[for=__toc]~.md-nav{display:flex}html [dir=rtl] .md-nav__link{padding-right:.8rem;padding-left:2.4rem}.md-nav__source{display:block;padding:0 .2rem;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color--dark)}}@media screen and (min-width: 60em){.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}}@media screen and (min-width: 76.25em){.md-nav{transition:max-height 250ms cubic-bezier(0.86, 0, 0.07, 1)}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}.md-nav__toggle~.md-nav{display:none}.md-nav__toggle:checked~.md-nav{display:block}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__icon{float:right;width:.9rem;height:.9rem;transition:transform 250ms}[dir=rtl] .md-nav__icon{float:left;transform:rotate(180deg)}.md-nav__icon::after{display:inline-block;width:100%;height:100%;vertical-align:-0.1rem;background-color:currentColor;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon{transform:rotate(90deg)}}:root{--md-search-result-icon: url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}.no-js .md-search{display:none}@media screen and (min-width: 60em){.md-search{padding:.2rem 0}}.md-search__overlay{z-index:1;opacity:0}@media screen and (max-width: 59.9375em){.md-search__overlay{position:absolute;top:.2rem;left:-2.2rem;width:2rem;height:2rem;overflow:hidden;background-color:var(--md-default-bg-color);border-radius:1rem;transform-origin:center;transition:transform 300ms 100ms,opacity 200ms 200ms;pointer-events:none}[dir=rtl] .md-search__overlay{right:-2.2rem;left:initial}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform 400ms,opacity 100ms}}@media screen and (max-width: 29.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width: 30em)and (max-width: 44.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width: 45em)and (max-width: 59.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}@media screen and (min-width: 60em){.md-search__overlay{position:fixed;top:0;left:0;width:0;height:0;background-color:rgba(0,0,0,.54);cursor:pointer;transition:width 0ms 250ms,height 0ms 250ms,opacity 250ms}[dir=rtl] .md-search__overlay{right:0;left:initial}[data-md-toggle=search]:checked~.md-header .md-search__overlay{width:100%;height:100%;opacity:1;transition:width 0ms,height 0ms,opacity 250ms}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width: 59.9375em){.md-search__inner{position:fixed;top:0;left:100%;z-index:2;width:100%;height:100%;transform:translateX(5%);opacity:0;transition:right 0ms 300ms,left 0ms 300ms,transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1),opacity 150ms 150ms}[data-md-toggle=search]:checked~.md-header .md-search__inner{left:0;transform:translateX(0);opacity:1;transition:right 0ms 0ms,left 0ms 0ms,transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms 150ms}[dir=rtl] [data-md-toggle=search]:checked~.md-header .md-search__inner{right:0;left:initial}html [dir=rtl] .md-search__inner{right:100%;left:initial;transform:translateX(-5%)}}@media screen and (min-width: 60em){.md-search__inner{position:relative;float:right;width:11.7rem;padding:.1rem 0;transition:width 250ms cubic-bezier(0.1, 0.7, 0.1, 1)}[dir=rtl] .md-search__inner{float:left}}@media screen and (min-width: 60em)and (max-width: 76.1875em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width: 76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{position:relative}@media screen and (min-width: 60em){.md-search__form{border-radius:.1rem}}.md-search__input{position:relative;z-index:2;padding:0 2.2rem 0 3.6rem;text-overflow:ellipsis;background-color:var(--md-default-bg-color);transition:color 250ms,background-color 250ms}[dir=rtl] .md-search__input{padding:0 3.6rem 0 2.2rem}.md-search__input::-webkit-input-placeholder{-webkit-transition:color 250ms;transition:color 250ms}.md-search__input::-moz-placeholder{-moz-transition:color 250ms;transition:color 250ms}.md-search__input::-ms-input-placeholder{-ms-transition:color 250ms;transition:color 250ms}.md-search__input::placeholder{transition:color 250ms}.md-search__input::-webkit-input-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-ms-input-placeholder{color:var(--md-default-fg-color--light)}.md-search__input~.md-search__icon,.md-search__input::placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width: 59.9375em){.md-search__input{width:100%;height:2.4rem;font-size:.9rem}}@media screen and (min-width: 60em){.md-search__input{width:100%;height:1.8rem;padding-left:2.2rem;color:inherit;font-size:.8rem;background-color:rgba(0,0,0,.26);border-radius:.1rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}.md-search__input::-webkit-input-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::-moz-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::-ms-input-placeholder{color:var(--md-primary-bg-color--light)}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input:hover{background-color:rgba(255,255,255,.12)}[data-md-toggle=search]:checked~.md-header .md-search__input{color:var(--md-default-fg-color);text-overflow:clip;background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0}[data-md-toggle=search]:checked~.md-header .md-search__input::-webkit-input-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::-moz-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::-ms-input-placeholder{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon,[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:var(--md-default-fg-color--light)}}.md-search__icon{position:absolute;z-index:2;width:1.2rem;height:1.2rem;cursor:pointer;transition:color 250ms,opacity 250ms}.md-search__icon:hover{opacity:.7}.md-search__icon[for=__search]{top:.3rem;left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem;left:initial}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width: 59.9375em){.md-search__icon[for=__search]{top:.6rem;left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem;left:initial}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width: 60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}.md-search__icon[type=reset]{top:.3rem;right:.5rem;transform:scale(0.75);opacity:0;transition:transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms;pointer-events:none}[dir=rtl] .md-search__icon[type=reset]{right:initial;left:.5rem}@media screen and (max-width: 59.9375em){.md-search__icon[type=reset]{top:.6rem;right:.8rem}[dir=rtl] .md-search__icon[type=reset]{right:initial;left:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__input:not(:-moz-placeholder-shown)~.md-search__icon[type=reset]{transform:scale(1);opacity:1;pointer-events:initial}[data-md-toggle=search]:checked~.md-header .md-search__input:not(:placeholder-shown)~.md-search__icon[type=reset]{transform:scale(1);opacity:1;pointer-events:initial}[data-md-toggle=search]:checked~.md-header .md-search__input:not(:-moz-placeholder-shown)~.md-search__icon[type=reset]:hover{opacity:.7}[data-md-toggle=search]:checked~.md-header .md-search__input:not(:placeholder-shown)~.md-search__icon[type=reset]:hover{opacity:.7}.md-search__output{position:absolute;z-index:1;width:100%;overflow:hidden;border-radius:0 0 .1rem .1rem}@media screen and (max-width: 59.9375em){.md-search__output{top:2.4rem;bottom:0}}@media screen and (min-width: 60em){.md-search__output{top:1.9rem;opacity:0;transition:opacity 400ms}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.4);opacity:1}}.md-search__scrollwrap{height:100%;overflow-y:auto;background-color:var(--md-default-bg-color);-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-scroll-snap-type:y mandatory;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory;touch-action:pan-y}@media(-webkit-max-device-pixel-ratio: 1), (max-resolution: 1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width: 60em)and (max-width: 76.1875em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width: 76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width: 60em){.md-search__scrollwrap{max-height:0;scrollbar-width:thin;scrollbar-color:var(--md-default-fg-color--lighter) transparent}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-search__scrollwrap::-webkit-scrollbar{width:.2rem;height:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{padding:0 .8rem;color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;background-color:var(--md-default-fg-color--lightest);scroll-snap-align:start}@media screen and (min-width: 60em){.md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem;padding-left:initial}}.md-search-result__list{margin:0;padding:0;list-style:none}.md-search-result__item{box-shadow:0 -0.05rem 0 var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;transition:background 250ms;scroll-snap-align:start}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:focus .md-search-result__article::before,.md-search-result__link:hover .md-search-result__article::before{opacity:.7}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more summary{display:block;padding:.75em .8rem;color:var(--md-typeset-a-color);font-size:.64rem;outline:0;cursor:pointer;transition:color 250ms,background-color 250ms;scroll-snap-align:start}.md-search-result__more summary:focus,.md-search-result__more summary:hover{color:var(--md-accent-fg-color);background-color:var(--md-accent-fg-color--transparent)}@media screen and (min-width: 60em){.md-search-result__more summary{padding-left:2.2rem}[dir=rtl] .md-search-result__more summary{padding-right:2.2rem;padding-left:.8rem}}.md-search-result__more summary::-webkit-details-marker{display:none}.md-search-result__more summary~*>*{opacity:.65}.md-search-result__article{position:relative;padding:0 .8rem;overflow:hidden}@media screen and (min-width: 60em){.md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem;padding-left:.8rem}}.md-search-result__article--document .md-search-result__title{margin:.55rem 0;font-weight:400;font-size:.8rem;line-height:1.4}.md-search-result__icon{position:absolute;left:0;width:1.2rem;height:1.2rem;margin:.5rem;color:var(--md-default-fg-color--light)}.md-search-result__icon::after{display:inline-block;width:100%;height:100%;background-color:currentColor;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}[dir=rtl] .md-search-result__icon{right:0;left:initial}[dir=rtl] .md-search-result__icon::after{transform:scaleX(-1)}@media screen and (max-width: 59.9375em){.md-search-result__icon{display:none}}.md-search-result__title{margin:.5em 0;font-weight:700;font-size:.64rem;line-height:1.6}.md-search-result__teaser{display:-webkit-box;max-height:2rem;margin:.5em 0;overflow:hidden;color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}@media screen and (max-width: 44.9375em){.md-search-result__teaser{max-height:3rem;-webkit-line-clamp:3}}@media screen and (min-width: 60em)and (max-width: 76.1875em){.md-search-result__teaser{max-height:3rem;-webkit-line-clamp:3}}.md-search-result__teaser mark{background-color:transparent;border-bottom:.05rem solid var(--md-accent-fg-color)}.md-search-result__terms{margin:.5em 0;font-size:.64rem;font-style:italic}.md-search-result mark{color:var(--md-accent-fg-color);background-color:transparent}@-webkit-keyframes md-sidebar__scrollwrap--hack{0%,99%{-webkit-scroll-snap-type:none;scroll-snap-type:none}100%{-webkit-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory}}@keyframes md-sidebar__scrollwrap--hack{0%,99%{-webkit-scroll-snap-type:none;-ms-scroll-snap-type:none;scroll-snap-type:none}100%{-webkit-scroll-snap-type:y mandatory;-ms-scroll-snap-type:y mandatory;scroll-snap-type:y mandatory}}.md-sidebar{position:-webkit-sticky;position:sticky;top:2.4rem;align-self:flex-start;width:12.1rem;padding:1.2rem 0;overflow:hidden}@media print{.md-sidebar{display:none}}@media screen and (max-width: 76.1875em){.md-sidebar--primary{position:fixed;top:0;left:-12.1rem;z-index:3;width:12.1rem;height:100%;background-color:var(--md-default-bg-color);transform:translateX(0);transition:transform 250ms cubic-bezier(0.4, 0, 0.2, 1),box-shadow 250ms}[dir=rtl] .md-sidebar--primary{right:-12.1rem;left:initial}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.4);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{overflow:hidden}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width: 60em){.md-sidebar--secondary{display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{max-height:100%;margin:0 .2rem;overflow-y:auto;-webkit-backface-visibility:hidden;backface-visibility:hidden;scrollbar-width:thin;scrollbar-color:var(--md-default-fg-color--lighter) transparent}.js .md-sidebar__scrollwrap{-webkit-animation:md-sidebar__scrollwrap--hack 400ms forwards;animation:md-sidebar__scrollwrap--hack 400ms forwards}@media screen and (max-width: 76.1875em){.md-sidebar--primary .md-sidebar__scrollwrap{position:absolute;top:0;right:0;bottom:0;left:0;margin:0;-webkit-scroll-snap-type:none;-ms-scroll-snap-type:none;scroll-snap-type:none}}.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-sidebar__scrollwrap::-webkit-scrollbar{width:.2rem;height:.2rem}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@-webkit-keyframes md-source__facts--done{0%{height:0}100%{height:.65rem}}@keyframes md-source__facts--done{0%{height:0}100%{height:.65rem}}@-webkit-keyframes md-source__fact--done{0%{transform:translateY(100%);opacity:0}50%{opacity:0}100%{transform:translateY(0%);opacity:1}}@keyframes md-source__fact--done{0%{transform:translateY(100%);opacity:0}50%{opacity:0}100%{transform:translateY(0%);opacity:1}}.md-source{display:block;font-size:.65rem;line-height:1.2;white-space:nowrap;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:opacity 250ms}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;width:2.4rem;height:2.4rem;vertical-align:middle}.md-source__icon svg{margin-top:.6rem;margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem;margin-left:initial}.md-source__icon+.md-source__repository{margin-left:-2rem;padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem;margin-left:initial;padding-right:2rem;padding-left:initial}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);margin-left:.6rem;overflow:hidden;font-weight:700;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{margin:0;padding:0;overflow:hidden;font-weight:700;font-size:.55rem;list-style-type:none;opacity:.75}[data-md-state=done] .md-source__facts{-webkit-animation:md-source__facts--done 250ms ease-in;animation:md-source__facts--done 250ms ease-in}.md-source__fact{float:left}[dir=rtl] .md-source__fact{float:right}[data-md-state=done] .md-source__fact{-webkit-animation:md-source__fact--done 400ms ease-out;animation:md-source__fact--done 400ms ease-out}.md-source__fact::before{margin:0 .1rem;content:"·"}.md-source__fact:first-child::before{display:none}.md-tabs{width:100%;overflow:auto;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color);transition:background 250ms}.no-js .md-tabs{transition:none}@media screen and (max-width: 76.1875em){.md-tabs{display:none}}@media print{.md-tabs{display:none}}.md-tabs__list{margin:0;margin-left:.2rem;padding:0;white-space:nowrap;list-style:none;contain:content}[dir=rtl] .md-tabs__list{margin-right:.2rem;margin-left:initial}.md-tabs__item{display:inline-block;height:2.4rem;padding-right:.6rem;padding-left:.6rem}.md-tabs__link{display:block;margin-top:.8rem;font-size:.7rem;opacity:.7;transition:transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 250ms}.no-js .md-tabs__link{transition:none}.md-tabs__link--active,.md-tabs__link:hover{color:inherit;opacity:1}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:100ms}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:120ms}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:140ms}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:160ms}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:180ms}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:200ms}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:220ms}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:240ms}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:260ms}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:280ms}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:300ms}.md-tabs[data-md-state=hidden]{pointer-events:none}.md-tabs[data-md-state=hidden] .md-tabs__link{transform:translateY(50%);opacity:0;transition:color 250ms,transform 0ms 400ms,opacity 100ms}@media screen and (min-width: 76.25em){.md-tabs~.md-main .md-nav--primary>.md-nav__list>.md-nav__item--nested{display:none}.md-tabs--active~.md-main .md-nav--primary .md-nav__title{display:block;padding:0 .6rem;pointer-events:none;scroll-snap-align:start}.md-tabs--active~.md-main .md-nav--primary .md-nav__title[for=__drawer]{display:none}.md-tabs--active~.md-main .md-nav--primary>.md-nav__list>.md-nav__item{display:none}.md-tabs--active~.md-main .md-nav--primary>.md-nav__list>.md-nav__item--active{display:block;padding:0}.md-tabs--active~.md-main .md-nav--primary>.md-nav__list>.md-nav__item--active>.md-nav__link{display:none}.md-tabs--active~.md-main .md-nav[data-md-level="1"]{display:block}.md-tabs--active~.md-main .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item{padding:0 .6rem}.md-tabs--active~.md-main .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item:last-child{padding-bottom:.6rem}.md-tabs--active~.md-main .md-nav[data-md-level="1"]>.md-nav__list>.md-nav__item:last-child .md-nav__item{padding-bottom:0}.md-tabs--active~.md-main .md-nav[data-md-level="1"] .md-nav .md-nav__title{display:none}}:root{--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{margin:1.5625em 0;padding:0 .6rem;overflow:hidden;color:var(--md-admonition-fg-color);font-size:.64rem;page-break-inside:avoid;background-color:var(--md-admonition-bg-color);border-left:.2rem solid #448aff;border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1)}[dir=rtl] .md-typeset .admonition,[dir=rtl] .md-typeset details{border-right:.2rem solid #448aff;border-left:none}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}.md-typeset .admonition .admonition,.md-typeset details .admonition,.md-typeset .admonition details,.md-typeset details details{margin:1em 0}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -0.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}.md-typeset .admonition-title,.md-typeset summary{position:relative;margin:0 -0.6rem 0 -0.8rem;padding:.4rem .6rem .4rem 2.2rem;font-weight:700;background-color:rgba(68,138,255,.1)}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{margin:0 -0.8rem 0 -0.6rem;padding:.4rem 2rem .4rem .6rem}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}.md-typeset .admonition-title::before,.md-typeset summary::before{position:absolute;left:.8rem;width:1rem;height:1rem;background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}[dir=rtl] .md-typeset .admonition-title::before,[dir=rtl] .md-typeset summary::before{right:.8rem;left:initial}.md-typeset .admonition-title code,.md-typeset summary code{margin:initial;padding:initial;color:currentColor;background-color:transparent;border-radius:initial;box-shadow:none}.md-typeset .admonition-title+.tabbed-set:last-child,.md-typeset summary+.tabbed-set:last-child{margin-top:0}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:rgba(68,138,255,.1)}.md-typeset .note>.admonition-title::before,.md-typeset .note>summary::before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.abstract,.md-typeset details.abstract,.md-typeset .admonition.tldr,.md-typeset details.tldr,.md-typeset .admonition.summary,.md-typeset details.summary{border-color:#00b0ff}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary,.md-typeset .tldr>.admonition-title,.md-typeset .tldr>summary,.md-typeset .summary>.admonition-title,.md-typeset .summary>summary{background-color:rgba(0,176,255,.1)}.md-typeset .abstract>.admonition-title::before,.md-typeset .abstract>summary::before,.md-typeset .tldr>.admonition-title::before,.md-typeset .tldr>summary::before,.md-typeset .summary>.admonition-title::before,.md-typeset .summary>summary::before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.info,.md-typeset details.info,.md-typeset .admonition.todo,.md-typeset details.todo{border-color:#00b8d4}.md-typeset .info>.admonition-title,.md-typeset .info>summary,.md-typeset .todo>.admonition-title,.md-typeset .todo>summary{background-color:rgba(0,184,212,.1)}.md-typeset .info>.admonition-title::before,.md-typeset .info>summary::before,.md-typeset .todo>.admonition-title::before,.md-typeset .todo>summary::before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.tip,.md-typeset details.tip,.md-typeset .admonition.important,.md-typeset details.important,.md-typeset .admonition.hint,.md-typeset details.hint{border-color:#00bfa5}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary,.md-typeset .important>.admonition-title,.md-typeset .important>summary,.md-typeset .hint>.admonition-title,.md-typeset .hint>summary{background-color:rgba(0,191,165,.1)}.md-typeset .tip>.admonition-title::before,.md-typeset .tip>summary::before,.md-typeset .important>.admonition-title::before,.md-typeset .important>summary::before,.md-typeset .hint>.admonition-title::before,.md-typeset .hint>summary::before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.success,.md-typeset details.success,.md-typeset .admonition.done,.md-typeset details.done,.md-typeset .admonition.check,.md-typeset details.check{border-color:#00c853}.md-typeset .success>.admonition-title,.md-typeset .success>summary,.md-typeset .done>.admonition-title,.md-typeset .done>summary,.md-typeset .check>.admonition-title,.md-typeset .check>summary{background-color:rgba(0,200,83,.1)}.md-typeset .success>.admonition-title::before,.md-typeset .success>summary::before,.md-typeset .done>.admonition-title::before,.md-typeset .done>summary::before,.md-typeset .check>.admonition-title::before,.md-typeset .check>summary::before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.question,.md-typeset details.question,.md-typeset .admonition.faq,.md-typeset details.faq,.md-typeset .admonition.help,.md-typeset details.help{border-color:#64dd17}.md-typeset .question>.admonition-title,.md-typeset .question>summary,.md-typeset .faq>.admonition-title,.md-typeset .faq>summary,.md-typeset .help>.admonition-title,.md-typeset .help>summary{background-color:rgba(100,221,23,.1)}.md-typeset .question>.admonition-title::before,.md-typeset .question>summary::before,.md-typeset .faq>.admonition-title::before,.md-typeset .faq>summary::before,.md-typeset .help>.admonition-title::before,.md-typeset .help>summary::before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.warning,.md-typeset details.warning,.md-typeset .admonition.attention,.md-typeset details.attention,.md-typeset .admonition.caution,.md-typeset details.caution{border-color:#ff9100}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary,.md-typeset .attention>.admonition-title,.md-typeset .attention>summary,.md-typeset .caution>.admonition-title,.md-typeset .caution>summary{background-color:rgba(255,145,0,.1)}.md-typeset .warning>.admonition-title::before,.md-typeset .warning>summary::before,.md-typeset .attention>.admonition-title::before,.md-typeset .attention>summary::before,.md-typeset .caution>.admonition-title::before,.md-typeset .caution>summary::before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.failure,.md-typeset details.failure,.md-typeset .admonition.missing,.md-typeset details.missing,.md-typeset .admonition.fail,.md-typeset details.fail{border-color:#ff5252}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary,.md-typeset .missing>.admonition-title,.md-typeset .missing>summary,.md-typeset .fail>.admonition-title,.md-typeset .fail>summary{background-color:rgba(255,82,82,.1)}.md-typeset .failure>.admonition-title::before,.md-typeset .failure>summary::before,.md-typeset .missing>.admonition-title::before,.md-typeset .missing>summary::before,.md-typeset .fail>.admonition-title::before,.md-typeset .fail>summary::before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.danger,.md-typeset details.danger,.md-typeset .admonition.error,.md-typeset details.error{border-color:#ff1744}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary,.md-typeset .error>.admonition-title,.md-typeset .error>summary{background-color:rgba(255,23,68,.1)}.md-typeset .danger>.admonition-title::before,.md-typeset .danger>summary::before,.md-typeset .error>.admonition-title::before,.md-typeset .error>summary::before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:rgba(245,0,87,.1)}.md-typeset .bug>.admonition-title::before,.md-typeset .bug>summary::before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.example,.md-typeset details.example{border-color:#651fff}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:rgba(101,31,255,.1)}.md-typeset .example>.admonition-title::before,.md-typeset .example>summary::before{background-color:#651fff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.md-typeset .admonition.quote,.md-typeset details.quote,.md-typeset .admonition.cite,.md-typeset details.cite{border-color:#9e9e9e}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary,.md-typeset .cite>.admonition-title,.md-typeset .cite>summary{background-color:rgba(158,158,158,.1)}.md-typeset .quote>.admonition-title::before,.md-typeset .quote>summary::before,.md-typeset .cite>.admonition-title::before,.md-typeset .cite>summary::before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.codehilite .o,.highlight .o,.codehilite .ow,.highlight .ow{color:var(--md-code-hl-operator-color)}.codehilite .p,.highlight .p{color:var(--md-code-hl-punctuation-color)}.codehilite .cpf,.highlight .cpf,.codehilite .l,.highlight .l,.codehilite .s,.highlight .s,.codehilite .sb,.highlight .sb,.codehilite .sc,.highlight .sc,.codehilite .s2,.highlight .s2,.codehilite .si,.highlight .si,.codehilite .s1,.highlight .s1,.codehilite .ss,.highlight .ss{color:var(--md-code-hl-string-color)}.codehilite .cp,.highlight .cp,.codehilite .se,.highlight .se,.codehilite .sh,.highlight .sh,.codehilite .sr,.highlight .sr,.codehilite .sx,.highlight .sx{color:var(--md-code-hl-special-color)}.codehilite .m,.highlight .m,.codehilite .mf,.highlight .mf,.codehilite .mh,.highlight .mh,.codehilite .mi,.highlight .mi,.codehilite .il,.highlight .il,.codehilite .mo,.highlight .mo{color:var(--md-code-hl-number-color)}.codehilite .k,.highlight .k,.codehilite .kd,.highlight .kd,.codehilite .kn,.highlight .kn,.codehilite .kp,.highlight .kp,.codehilite .kr,.highlight .kr,.codehilite .kt,.highlight .kt{color:var(--md-code-hl-keyword-color)}.codehilite .kc,.highlight .kc,.codehilite .n,.highlight .n{color:var(--md-code-hl-name-color)}.codehilite .no,.highlight .no,.codehilite .nb,.highlight .nb,.codehilite .bp,.highlight .bp{color:var(--md-code-hl-constant-color)}.codehilite .nc,.highlight .nc,.codehilite .ne,.highlight .ne,.codehilite .nf,.highlight .nf,.codehilite .nn,.highlight .nn{color:var(--md-code-hl-function-color)}.codehilite .nd,.highlight .nd,.codehilite .ni,.highlight .ni,.codehilite .nl,.highlight .nl,.codehilite .nt,.highlight .nt{color:var(--md-code-hl-keyword-color)}.codehilite .c,.highlight .c,.codehilite .cm,.highlight .cm,.codehilite .c1,.highlight .c1,.codehilite .ch,.highlight .ch,.codehilite .cs,.highlight .cs,.codehilite .sd,.highlight .sd{color:var(--md-code-hl-comment-color)}.codehilite .na,.highlight .na,.codehilite .nv,.highlight .nv,.codehilite .vc,.highlight .vc,.codehilite .vg,.highlight .vg,.codehilite .vi,.highlight .vi{color:var(--md-code-hl-variable-color)}.codehilite .ge,.highlight .ge,.codehilite .gr,.highlight .gr,.codehilite .gh,.highlight .gh,.codehilite .go,.highlight .go,.codehilite .gp,.highlight .gp,.codehilite .gs,.highlight .gs,.codehilite .gu,.highlight .gu,.codehilite .gt,.highlight .gt{color:var(--md-code-hl-generic-color)}.codehilite .gd,.highlight .gd,.codehilite .gi,.highlight .gi{margin:0 -0.125em;padding:0 .125em;border-radius:.1rem}.codehilite .gd,.highlight .gd{background-color:var(--md-typeset-del-color)}.codehilite .gi,.highlight .gi{background-color:var(--md-typeset-ins-color)}.codehilite .hll,.highlight .hll{display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em;background-color:var(--md-code-hl-color)}.codehilitetable,.highlighttable{display:block;overflow:hidden}.codehilitetable tbody,.highlighttable tbody,.codehilitetable td,.highlighttable td{display:block;padding:0}.codehilitetable tr,.highlighttable tr{display:flex}.codehilitetable pre,.highlighttable pre{margin:0}.codehilitetable .linenos,.highlighttable .linenos{padding:.525rem 1.1764705882em;padding-right:0;font-size:.85em;background-color:var(--md-code-bg-color);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.codehilitetable .linenodiv,.highlighttable .linenodiv{padding-right:.5882352941em;box-shadow:-0.05rem 0 var(--md-default-fg-color--lighter) inset}.codehilitetable .linenodiv pre,.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.codehilitetable .code,.highlighttable .code{flex:1;overflow:hidden}.md-typeset .codehilitetable,.md-typeset .highlighttable{margin:1em 0;direction:ltr;border-radius:.1rem}.md-typeset .codehilitetable code,.md-typeset .highlighttable code{border-radius:0}@media screen and (max-width: 44.9375em){.md-typeset>.codehilite,.md-typeset>.highlight{margin:1em -0.8rem}.md-typeset>.codehilite .hll,.md-typeset>.highlight .hll{margin:0 -0.8rem;padding:0 .8rem}.md-typeset>.codehilite code,.md-typeset>.highlight code{border-radius:0}.md-typeset>.codehilitetable,.md-typeset>.highlighttable{margin:1em -0.8rem;border-radius:0}.md-typeset>.codehilitetable .hll,.md-typeset>.highlighttable .hll{margin:0 -0.8rem;padding:0 .8rem}}:root{--md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,')}.md-typeset [id^="fnref:"]{display:inline-block}.md-typeset [id^="fnref:"]:target{margin-top:-3.8rem;padding-top:3.8rem;pointer-events:none;scroll-margin-top:initial}.md-typeset [id^="fn:"]::before{display:none;height:0;content:""}.md-typeset [id^="fn:"]:target{scroll-margin-top:initial}.md-typeset [id^="fn:"]:target::before{display:block;margin-top:-3.5rem;padding-top:3.5rem;pointer-events:none}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}.md-typeset .footnote ol{margin-left:0}.md-typeset .footnote li{transition:color 125ms}.md-typeset .footnote li:target{color:var(--md-default-fg-color)}.md-typeset .footnote li :first-child{margin-top:0}.md-typeset .footnote li:hover .footnote-backref,.md-typeset .footnote li:target .footnote-backref{transform:translateX(0);opacity:1}.md-typeset .footnote li:hover .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-ref{display:inline-block;pointer-events:initial}.md-typeset .footnote-backref{display:inline-block;color:var(--md-typeset-a-color);font-size:0;vertical-align:text-bottom;transform:translateX(0.25rem);opacity:0;transition:color 250ms,transform 250ms 250ms,opacity 125ms 250ms}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-0.25rem)}.md-typeset .footnote-backref::before{display:inline-block;width:.8rem;height:.8rem;background-color:currentColor;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}[dir=rtl] .md-typeset .footnote-backref::before svg{transform:scaleX(-1)}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);transform:translateX(0);opacity:1}}.md-typeset .headerlink{display:inline-block;margin-left:.5rem;visibility:hidden;opacity:0;transition:color 250ms,visibility 0ms 500ms,opacity 125ms}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem;margin-left:initial}html body .md-typeset .headerlink{color:var(--md-default-fg-color--lighter)}@media print{.md-typeset .headerlink{display:none}}.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink,.md-typeset .headerlink:focus{visibility:visible;opacity:1;transition:color 250ms,visibility 0ms,opacity 125ms}.md-typeset :target>.headerlink,.md-typeset .headerlink:focus,.md-typeset .headerlink:hover{color:var(--md-accent-fg-color)}.md-typeset :target{scroll-margin-top:3.6rem}.md-typeset h3[id]:target,.md-typeset h2[id]:target,.md-typeset h1[id]:target{scroll-margin-top:initial}.md-typeset h3[id]::before,.md-typeset h2[id]::before,.md-typeset h1[id]::before{display:block;margin-top:-0.4rem;padding-top:.4rem;content:""}.md-typeset h3[id]:target::before,.md-typeset h2[id]:target::before,.md-typeset h1[id]:target::before{margin-top:-3.4rem;padding-top:3.4rem}.md-typeset h4[id]:target{scroll-margin-top:initial}.md-typeset h4[id]::before{display:block;margin-top:-0.45rem;padding-top:.45rem;content:""}.md-typeset h4[id]:target::before{margin-top:-3.45rem;padding-top:3.45rem}.md-typeset h6[id]:target,.md-typeset h5[id]:target{scroll-margin-top:initial}.md-typeset h6[id]::before,.md-typeset h5[id]::before{display:block;margin-top:-0.6rem;padding-top:.6rem;content:""}.md-typeset h6[id]:target::before,.md-typeset h5[id]:target::before{margin-top:-3.6rem;padding-top:3.6rem}.md-typeset div.arithmatex{overflow-x:scroll}@media screen and (max-width: 44.9375em){.md-typeset div.arithmatex{margin:0 -0.8rem}}.md-typeset div.arithmatex>*{width:-webkit-min-content;width:-moz-min-content;width:min-content;margin:1em auto !important;padding:0 .8rem;overflow:auto;touch-action:auto}.md-typeset del.critic,.md-typeset ins.critic,.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment::before{content:"/* "}.md-typeset .critic.comment::after{content:" */"}.md-typeset .critic.block{display:block;margin:1em 0;padding-right:.8rem;padding-left:.8rem;overflow:auto;box-shadow:none}.md-typeset .critic.block :first-child{margin-top:.5em}.md-typeset .critic.block :last-child{margin-bottom:.5em}:root{--md-details-icon: url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:block;padding-top:0;overflow:visible}.md-typeset details[open]>summary::after{transform:rotate(90deg)}.md-typeset details:not([open]){padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}.md-typeset details::after{display:table;content:""}.md-typeset summary{display:block;min-height:1rem;padding:.4rem 1.8rem .4rem 2.2rem;border-top-left-radius:.1rem;border-top-right-radius:.1rem;cursor:pointer}.md-typeset summary:not(.focus-visible){outline:none;-webkit-tap-highlight-color:transparent}[dir=rtl] .md-typeset summary{padding:.4rem 2.2rem .4rem 1.8rem}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset summary::after{position:absolute;top:.4rem;right:.4rem;width:1rem;height:1rem;background-color:currentColor;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;transform:rotate(0deg);transition:transform 250ms;content:""}[dir=rtl] .md-typeset summary::after{right:initial;left:.4rem;transform:rotate(180deg)}.md-typeset img.emojione,.md-typeset img.twemoji,.md-typeset img.gemoji{width:1.125em;max-height:100%;vertical-align:-15%}.md-typeset span.twemoji{display:inline-block;height:1.125em;vertical-align:text-top}.md-typeset span.twemoji svg{width:1.125em;max-height:100%;fill:currentColor}.highlight [data-linenos]::before{position:-webkit-sticky;position:sticky;left:-1.1764705882em;float:left;margin-right:1.1764705882em;margin-left:-1.1764705882em;padding-left:1.1764705882em;color:var(--md-default-fg-color--light);background-color:var(--md-code-bg-color);box-shadow:-0.05rem 0 var(--md-default-fg-color--lighter) inset;content:attr(data-linenos);-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.md-typeset .keys kbd::before,.md-typeset .keys kbd::after{position:relative;margin:0;color:inherit;-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial}.md-typeset .keys span{padding:0 .2em;color:var(--md-default-fg-color--light)}.md-typeset .keys .key-alt::before{padding-right:.4em;content:"⎇"}.md-typeset .keys .key-left-alt::before{padding-right:.4em;content:"⎇"}.md-typeset .keys .key-right-alt::before{padding-right:.4em;content:"⎇"}.md-typeset .keys .key-command::before{padding-right:.4em;content:"⌘"}.md-typeset .keys .key-left-command::before{padding-right:.4em;content:"⌘"}.md-typeset .keys .key-right-command::before{padding-right:.4em;content:"⌘"}.md-typeset .keys .key-control::before{padding-right:.4em;content:"⌃"}.md-typeset .keys .key-left-control::before{padding-right:.4em;content:"⌃"}.md-typeset .keys .key-right-control::before{padding-right:.4em;content:"⌃"}.md-typeset .keys .key-meta::before{padding-right:.4em;content:"◆"}.md-typeset .keys .key-left-meta::before{padding-right:.4em;content:"◆"}.md-typeset .keys .key-right-meta::before{padding-right:.4em;content:"◆"}.md-typeset .keys .key-option::before{padding-right:.4em;content:"⌥"}.md-typeset .keys .key-left-option::before{padding-right:.4em;content:"⌥"}.md-typeset .keys .key-right-option::before{padding-right:.4em;content:"⌥"}.md-typeset .keys .key-shift::before{padding-right:.4em;content:"⇧"}.md-typeset .keys .key-left-shift::before{padding-right:.4em;content:"⇧"}.md-typeset .keys .key-right-shift::before{padding-right:.4em;content:"⇧"}.md-typeset .keys .key-super::before{padding-right:.4em;content:"❖"}.md-typeset .keys .key-left-super::before{padding-right:.4em;content:"❖"}.md-typeset .keys .key-right-super::before{padding-right:.4em;content:"❖"}.md-typeset .keys .key-windows::before{padding-right:.4em;content:"⊞"}.md-typeset .keys .key-left-windows::before{padding-right:.4em;content:"⊞"}.md-typeset .keys .key-right-windows::before{padding-right:.4em;content:"⊞"}.md-typeset .keys .key-arrow-down::before{padding-right:.4em;content:"↓"}.md-typeset .keys .key-arrow-left::before{padding-right:.4em;content:"←"}.md-typeset .keys .key-arrow-right::before{padding-right:.4em;content:"→"}.md-typeset .keys .key-arrow-up::before{padding-right:.4em;content:"↑"}.md-typeset .keys .key-backspace::before{padding-right:.4em;content:"⌫"}.md-typeset .keys .key-backtab::before{padding-right:.4em;content:"⇤"}.md-typeset .keys .key-caps-lock::before{padding-right:.4em;content:"⇪"}.md-typeset .keys .key-clear::before{padding-right:.4em;content:"⌧"}.md-typeset .keys .key-context-menu::before{padding-right:.4em;content:"☰"}.md-typeset .keys .key-delete::before{padding-right:.4em;content:"⌦"}.md-typeset .keys .key-eject::before{padding-right:.4em;content:"⏏"}.md-typeset .keys .key-end::before{padding-right:.4em;content:"⤓"}.md-typeset .keys .key-escape::before{padding-right:.4em;content:"⎋"}.md-typeset .keys .key-home::before{padding-right:.4em;content:"⤒"}.md-typeset .keys .key-insert::before{padding-right:.4em;content:"⎀"}.md-typeset .keys .key-page-down::before{padding-right:.4em;content:"⇟"}.md-typeset .keys .key-page-up::before{padding-right:.4em;content:"⇞"}.md-typeset .keys .key-print-screen::before{padding-right:.4em;content:"⎙"}.md-typeset .keys .key-tab::after{padding-left:.4em;content:"⇥"}.md-typeset .keys .key-num-enter::after{padding-left:.4em;content:"⌤"}.md-typeset .keys .key-enter::after{padding-left:.4em;content:"⏎"}.md-typeset .tabbed-content{display:none;order:99;width:100%;box-shadow:0 -0.05rem var(--md-default-fg-color--lightest)}.md-typeset .tabbed-content>pre:only-child,.md-typeset .tabbed-content>.codehilite:only-child pre,.md-typeset .tabbed-content>.codehilitetable:only-child,.md-typeset .tabbed-content>.highlight:only-child pre,.md-typeset .tabbed-content>.highlighttable:only-child{margin:0}.md-typeset .tabbed-content>pre:only-child>code,.md-typeset .tabbed-content>.codehilite:only-child pre>code,.md-typeset .tabbed-content>.codehilitetable:only-child>code,.md-typeset .tabbed-content>.highlight:only-child pre>code,.md-typeset .tabbed-content>.highlighttable:only-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-content>.tabbed-set{margin:0}.md-typeset .tabbed-set{position:relative;display:flex;flex-wrap:wrap;margin:1em 0;border-radius:.1rem}.md-typeset .tabbed-set>input{position:absolute;width:0;height:0;opacity:0}.md-typeset .tabbed-set>input:checked+label{color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:checked+label+.tabbed-content{display:block}.md-typeset .tabbed-set>input:focus+label{outline-style:auto}.md-typeset .tabbed-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.md-typeset .tabbed-set>label{z-index:1;width:auto;padding:.9375em 1.25em .78125em;color:var(--md-default-fg-color--light);font-weight:700;font-size:.64rem;border-bottom:.1rem solid transparent;cursor:pointer;transition:color 250ms}html .md-typeset .tabbed-set>label:hover{color:var(--md-accent-fg-color)}:root{--md-tasklist-icon: url( 'data:image/svg+xml;charset=utf-8,' );--md-tasklist-icon--checked: url( 'data:image/svg+xml;charset=utf-8,' )}.md-typeset .task-list-item{position:relative;list-style-type:none}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em;left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em;left:initial}.md-typeset .task-list-control .task-list-indicator::before{position:absolute;top:.15em;left:-1.5em;width:1.25em;height:1.25em;background-color:var(--md-default-fg-color--lightest);-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;content:""}[dir=rtl] .md-typeset .task-list-control .task-list-indicator::before{right:-1.5em;left:initial}.md-typeset .task-list-control [type=checkbox]:checked+.task-list-indicator::before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}.md-typeset .task-list-control [type=checkbox]{z-index:-1;opacity:0} + +/*# sourceMappingURL=main.38780c08.min.css.map*/ \ No newline at end of file diff --git a/v2/assets/stylesheets/main.38780c08.min.css.map b/v2/assets/stylesheets/main.38780c08.min.css.map new file mode 100644 index 000000000..964fe38e7 --- /dev/null +++ b/v2/assets/stylesheets/main.38780c08.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///src/assets/stylesheets/main.scss","webpack:///src/assets/stylesheets/main/_reset.scss","webpack:///src/assets/stylesheets/main/_colors.scss","webpack:///src/assets/stylesheets/main/_icons.scss","webpack:///src/assets/stylesheets/main/_typeset.scss","webpack:///src/assets/stylesheets/utilities/_break.scss","webpack:///node_modules/material-shadows/material-shadows.scss","webpack:///src/assets/stylesheets/main/layout/_base.scss","webpack:///src/assets/stylesheets/main/layout/_announce.scss","webpack:///src/assets/stylesheets/main/layout/_button.scss","webpack:///src/assets/stylesheets/main/layout/_clipboard.scss","webpack:///src/assets/stylesheets/main/layout/_content.scss","webpack:///src/assets/stylesheets/main/layout/_dialog.scss","webpack:///src/assets/stylesheets/main/layout/_header.scss","webpack:///src/assets/stylesheets/main/layout/_footer.scss","webpack:///src/assets/stylesheets/main/layout/_nav.scss","webpack:///src/assets/stylesheets/main/layout/_search.scss","webpack:///src/assets/stylesheets/main/layout/_sidebar.scss","webpack:///src/assets/stylesheets/main/layout/_source.scss","webpack:///src/assets/stylesheets/main/layout/_tabs.scss","webpack:///src/assets/stylesheets/main/extensions/markdown/_admonition.scss","webpack:///node_modules/material-design-color/material-color.scss","webpack:///src/assets/stylesheets/main/extensions/markdown/_codehilite.scss","webpack:///src/assets/stylesheets/main/extensions/markdown/_footnotes.scss","webpack:///src/assets/stylesheets/main/extensions/markdown/_toc.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_critic.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_details.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_emoji.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_highlight.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_keys.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss","webpack:///src/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss"],"names":[],"mappings":"AAAA,KC6BA,qBACE,8BACA,CADA,0BACA,CADA,yBACA,CADA,qBACA,sBAIF,kBAGE,MAIF,QACE,IAIF,sBACE,iBACA,sBAIF,uCAIE,GAIF,aACE,qBACA,OAIF,aACE,SAIF,eAEE,KAIF,iBACE,OAIF,wBACE,iBACA,OAIF,kBAEE,mBACA,QAIF,QACE,UACA,kBACA,uBACA,SACA,OAIF,QACE,aACA,OChFF,4CAGE,oDACA,sDACA,uDACA,4CACA,qDACA,uDACA,yDACA,8CAGA,qDACA,oDACA,4CACA,qDACA,6CAGA,4DACA,2CACA,oDACA,SAGA,0CAGE,wCACA,6CAGA,gDACA,mDACA,oDACA,oDACA,mDACA,kDACA,iDACA,+DACA,kEACA,8DACA,8DACA,+DACA,+CAGA,iDACA,kDAGA,gDAGA,kDACA,4CAGA,sDACA,mDACA,qDAGA,qDACA,2CAGA,oDACA,sDACA,4CACA,kDACA,cCrEF,aACE,aACA,cACA,cACA,kBACA,MCRJ,kCACE,kCACA,YAIF,6BAEE,oCACA,wEACA,cAIF,6BAGE,6BACA,oDACA,OAQF,uNACE,0NACA,aASF,eACE,gBACA,iCACA,CADA,kBACA,cAIA,YAPF,gBAQI,qEAIF,YAIE,gBAIF,iBACE,wCACA,gBACA,cACA,gBACA,uBACA,gBAIF,oBACE,gBACA,mBACA,gBACA,uBACA,gBAIF,mBACE,gBACA,iBACA,gBACA,uBACA,mBAIF,eACE,gBAIF,YACE,gBACA,uBACA,+BAIF,eAEE,wCACA,gBACA,eACA,uBACA,gBAIF,wBACE,gBAIF,cACE,gEACA,eAIF,+BACE,sBACA,qCAGA,sBAEE,yCAIF,+BAEE,kDAKJ,6BAGE,cACA,cAGA,iDAPF,oBAQI,mBAKJ,uBACE,gBACA,sBACA,yCACA,oBACA,mCACA,CADA,0BACA,yHAIF,cAME,gBACA,6BACA,gBACA,oBAIF,kBACE,iBAIF,iBACE,aACA,gBACA,sBAGA,aACE,SACA,qCACA,cACA,kBACA,gBACA,mCACA,CADA,0BACA,kBACA,qBAEA,gEACA,4BAGA,qDACE,yCAIF,WACE,aACA,+CAIF,oDACE,qDAGA,0CACE,0CCpCN,gBD8CA,kBACE,sBAGA,eACE,kBAMN,oBACE,wBACA,iCACA,gBACA,wBACA,sBACA,6CACA,oBACA,qKAEE,kBAMJ,aACE,sBACA,8CACA,mCACA,CADA,0BACA,kBAIF,oBACE,8DACA,YACA,qBAGA,iBANF,iBAOI,2EAGA,gGE/QJ,kBFmRM,OACA,qBACA,WACA,8BACA,CADA,0BACA,CADA,qBACA,cACA,eACA,oBACA,iCACA,gBACA,sCACA,oBACA,oBACA,oBAON,WACE,iCAIF,qBAEE,qDAGA,sBACE,oBACA,wBAKJ,kBACE,wCACA,4DACA,kCAGA,mBACE,qBACA,6DACA,oBACA,gBAKJ,oBACE,+BAIF,kBAEE,UACA,mDAGA,mBACE,oBACA,qCAIF,2BACE,2CAGA,2BACE,qCAKJ,kBACE,mBACA,yDAGA,mBACE,oBACA,mGAIF,aAEE,2DAIF,eACE,qFAIF,yBAEE,6HAGA,mBACE,oBACA,gBAOR,0BACE,0BAGA,oBACE,oBACA,iCAKJ,cAEE,YACA,yDAGA,UACE,cACA,2DAIF,UACE,eACA,qEAIF,YACE,oBAKJ,yBACE,CADF,sBACE,CADF,iBACE,eACA,cACA,kBACA,wBAIF,eACE,qBACA,kBACA,oBAIF,cACE,gCAIF,oBACE,eACA,cACA,iBACA,sCACA,oBACA,mEAEE,kBAEF,cAGA,+BAbF,aAcI,mCAMF,gBACE,iGAQA,YACE,+FAIF,eACE,+FAKJ,eAEE,mHAGA,gBACE,mCAKJ,cACE,uBACA,iCACA,mBACA,mDACA,qCAGA,aACE,mCAKJ,sBACE,mBACA,6DACA,mCAIF,iCACE,yCAGA,iCACE,uDACA,kDAIF,YACE,kCAKJ,iBACE,yCAKJ,cACE,gDAGA,oBACE,YACA,aACA,iBACA,mBACA,8BACA,CADA,qBACA,YACA,qEAIF,6BACE,sDACA,CADA,6CACA,sEAIF,6BACE,uDACA,CADA,8CACA,yBAKJ,kBACE,gBACA,kBACA,oBAIF,oBACE,mBACA,gBACA,cAGA,mBANF,aAOI,gCAIF,aACE,WACA,SACA,gBACA,MGjkBN,WACE,kBAKA,eAOA,sCF0IE,KEvJJ,gBAiBI,uCFsIA,KEvJJ,cAsBI,OAKJ,iBACE,aACA,sBACA,WACA,gBACA,gBAGA,4CACA,0CFqIE,yBE/HA,cACE,eAMJ,KAtBF,aAuBI,KAKJ,aACE,cACA,UACA,SACA,UAIF,eACE,kBACA,iBACA,eAIF,YACE,sBACA,YACA,cAIA,cAPF,aAQI,WAKJ,WACE,iBAGA,YACE,YACA,kBACA,cAKJ,aACE,gBACA,mBACA,uBACA,YAQF,YACE,aAIF,cACE,MACA,UACA,QACA,SACA,iCACA,UACA,0DAEE,0CFgDA,4CExCA,UACE,YACA,UACA,8CAEE,WAYR,cACE,WAGA,aACA,oBACA,iCACA,iBACA,4CACA,oBACA,6BACA,UACA,gBAGA,UACE,wBACA,UACA,2EAEE,OAUN,WACE,cC1LF,aACE,2CACA,qBAGA,iBACE,gBACA,gCACA,gBACA,cAIF,aAbF,YAcI,yBCXF,oBACE,mBACA,iCACA,gBACA,gCACA,oBACA,iEAEE,iCAKF,gCACE,4CACA,wCACA,2DAIF,+BAEE,2CACA,uCACA,OC3BN,yPACE,eAMF,iBACE,SACA,WACA,UACA,YACA,aACA,2CACA,oBAEA,eACA,uBACA,cAGA,cAdF,YAeI,uBASF,aACE,cACA,eACA,cACA,8BACA,4CACA,CADA,mCACA,8BACA,CADA,qBACA,WACA,yBAIF,uCACE,iDAIF,+BAEE,aClDJ,MACE,eACA,+DNyII,YM3IN,8BAMI,yCN0JA,YMhKJ,kCAWI,qBAIF,qBACE,kBACA,wCN+IA,mBMjJF,mBAMI,mBACA,6BAKF,aACE,aACA,WACA,gCAIF,eACE,qBAKJ,WACE,eACA,kBACA,UACA,+BAGA,UACE,mBACA,oBACA,mCAGA,oBACE,iCAKJ,yCACE,yBAIF,cACE,mBACA,cAIF,oBA9BF,YA+BI,aCvEN,gGNFE,eMKA,YACA,aACA,aACA,UACA,cACA,kBACA,oBACA,iCACA,gBACA,sCACA,YACA,oBACA,2BACA,UACA,6CAEE,sBAIF,aACE,WACA,gCAIF,uBACE,UACA,6EAEE,cAKJ,WAtCF,YAuCI,aCvCJ,uBACE,CADF,eACE,MACA,QACA,OACA,UACA,cACA,iCACA,4CACA,+DAIE,8CAGA,mBAIF,eACE,gBACA,kCAIF,gEAEI,+DAGA,cAMJ,WApCF,YAqCI,iBAKJ,YACE,gBACA,wBAGA,iBACE,UACA,cACA,aACA,cACA,eACA,yBACA,sCAME,oBACE,2DAKJ,UAEE,gCAIF,YACE,cACA,uEAGA,aAEE,aACA,cACA,kBACA,6CAKJ,YACE,qCRwEF,qCQjEE,YACE,2CRkFJ,+BQ1EE,YACE,yCRuDJ,qCQ/CE,YACE,wBAMN,iBACE,WACA,wEAEE,6CAIF,UACE,8BACA,UACA,wEAEE,oBAEF,uDAGA,8BACE,8BAKJ,gBACE,oDAIF,YACE,uBAKJ,WACE,eACA,gBACA,mBACA,mEAGA,UACE,+BACA,UACA,wEAEE,oBAEF,6EAGA,6BACE,yFAIF,SACE,wBACA,UACA,wEAEE,uBAEF,gDAKJ,iBACE,WACA,YACA,wBAKJ,YACE,qCRtCA,uBQqCF,aAKI,cACA,kBACA,iBACA,kCAGA,iBACE,oBACA,yCRlDJ,uBQqCF,kBAmBI,kCAGA,mBACE,aC5NR,+BACE,2CACA,cAGA,WALF,YAMI,wBAQF,aACE,cACA,sBAIF,YACE,mBACA,qBACA,yBACA,qCTwIA,qBS5IF,SAQI,wDAIF,UAEE,4BAIF,UACE,sCAGA,WACE,0CAGA,oBACE,0CTmIN,2BS5IA,SAeI,kDAGA,YACE,6BAMN,WACE,iBACA,sCAGA,UACE,gBACA,0CAGA,oBACE,0CTwGN,2BSnHA,SAiBI,wBAMN,iBACE,YACA,8BACA,eACA,gBACA,mBACA,wBAIF,YACE,cACA,2BAIF,iBACE,QACA,OACA,iBACA,eACA,iBACA,WACA,iBAKJ,gDACE,wBAGA,YACE,eACA,8BACA,cACA,mCAIF,sCACE,iFAGA,+BAEE,sBAMN,UACE,kBACA,gBACA,yCACA,iBACA,qCTiBE,qBStBJ,UASI,kCAIF,sCACE,mBAKJ,cACE,sBACA,qCTCE,kBSHJ,eAMI,0BAIF,oBACE,aACA,cACA,kBACA,iCAGA,eACE,6BAIF,gBACE,oBACA,kBACA,OCtLN,2MACE,kMACA,2NACA,SAMF,eACE,gBACA,gBAGA,aACE,gBACA,gBACA,gBACA,uBACA,gCAGA,YACE,oCAGA,UACE,YACA,uFAOA,aAEE,aACA,cACA,4CAIF,iBACE,eAOR,QACE,UACA,gBACA,eAIF,eACE,0BAGA,oBACE,6BAIF,eACE,uCAGA,mBACE,eACA,wCAIF,gBACE,eAMN,aACE,kBACA,gBACA,uBACA,eACA,uBACA,wBACA,+BAIA,YACE,uCAGA,YACE,mCAKJ,uCACE,qCAIF,+BACE,qCAIF,aACE,yCAIF,+BAEE,iBAKJ,YACE,0CVkDA,QUzKJ,2CA4HI,2CAGA,iBAEE,MACA,QACA,OACA,UACA,aACA,sBACA,YACA,gEAOA,eAEE,gBACA,iCAIF,iBACE,cACA,yBACA,wCACA,gBACA,mBACA,mBACA,sDACA,eACA,+CAGA,iBACE,UACA,WACA,cACA,aACA,cACA,aACA,sDAGA,aACE,WACA,YACA,8BACA,4CACA,CADA,mCACA,8BACA,CADA,qBACA,WACA,yDAIF,WACE,aACA,+CAKJ,eACE,4CACA,iEAEE,qCACF,CADE,gCACF,CADE,4BACF,mBACA,yEAGA,YACE,+CAKJ,iBACE,iCACA,4CACA,+DAGA,iBACE,UACA,WACA,cACA,aACA,cACA,iBACA,8EASJ,WACE,aACA,gCAKJ,MACE,gCAIF,SACE,6DACA,0CAGA,SACE,sDAIF,oBACE,gEAGA,mBACE,oBACA,sDAKJ,+BACE,uHAGA,+BAEE,gCAMN,iBACE,aACA,oBACA,8CAGA,iBACE,QACA,YACA,aACA,cACA,mBACA,cACA,iBACA,qDAGA,aACE,WACA,YACA,8BACA,4CACA,CADA,mCACA,8BACA,CADA,qBACA,WACA,wDAIF,aACE,WACA,iDASJ,mBACE,mDAQF,eACE,6CAIF,eACE,6BACA,2DAGA,mBACE,qEAGA,oBACE,qBACA,mEAKJ,iBACE,6EAGA,kBACE,qBACA,2EAKJ,mBACE,qFAGA,oBACE,qBACA,mFAKJ,mBACE,6FAGA,oBACE,qBACA,yBAQV,YACE,2BACA,UACA,2EAEE,mCAIF,2BACE,iCAKJ,uBACE,UACA,4EAEE,+CAIF,kCACE,CADF,0BACE,2CVxOJ,8BUkPA,aACE,qBACA,6CAGA,YACE,+CAIF,aACE,WACA,YACA,sCACA,CADA,6BACA,8BACA,WACA,uCAIF,YACE,8BAKJ,mBACE,oBACA,iBAIF,aACE,gBACA,iCACA,kDACA,sCVxSF,6CUmTE,uBACE,iDAIF,YACE,yCVzTJ,QUvJJ,0DAudI,+CAME,uBACE,+CAIF,YACE,yBAKJ,YACE,iCAIF,aACE,8CAIF,YACE,eAIF,WACE,YACA,aACA,2BACA,yBAGA,UACE,yBACA,sBAIF,oBACE,WACA,YACA,uBACA,8BACA,4CACA,CADA,mCACA,8BACA,CADA,qBACA,WACA,2EAIF,uBACE,QClhBR,yfACE,YAMF,iBACE,mBAGA,YACE,qCX4IA,WWjJJ,eAUI,sBAIF,SACE,UACA,0CXmJA,oBWrJF,iBAMI,UACA,aACA,WACA,YACA,gBACA,4CACA,mBACA,wBACA,qDAEE,oBAEF,+BAGA,aACE,aACA,gEAIF,SACE,yCAEE,2CXuHN,+DWjHA,mBAII,gEXsEF,+DW1EF,mBASI,gEXiEF,+DW1EF,mBAcI,sCXiFJ,oBWnIF,cAwDI,MACA,OACA,QACA,SACA,iCACA,eACA,0DAEE,+BAKF,OACE,aACA,gEAIF,UACE,YACA,UACA,8CAEE,oBAQR,kCAEE,CAFF,0BAEE,0CX2DA,kBW7DF,cAMI,MACA,UACA,UACA,WACA,YACA,yBACA,UACA,iHAEE,8DAMF,MACE,wBACA,UACA,+GAEE,wEAMF,OACE,aACA,kCAKJ,UACE,aACA,0BACA,sCXCJ,kBW3CF,iBAgDI,YACA,cACA,gBACA,sDACA,6BAGA,UACE,gEXlCF,6DWuCF,aAII,yCXtBJ,6DWkBA,aASI,mBAMN,iBACE,qCXlCA,iBWiCF,mBAKI,oBAKJ,iBACE,UACA,0BACA,uBACA,4CACA,8CAEE,6BAIF,yBACE,8CAIF,8BACE,CADF,sBACE,CALA,oCAIF,2BACE,CADF,sBACE,CALA,yCAIF,0BACE,CADF,sBACE,CALA,+BAIF,sBACE,8CAIF,uCAEE,CANA,oCAIF,uCAEE,CANA,yCAIF,uCAEE,CANA,kEAIF,uCAEE,8BAIF,YACE,0CXrDF,kBWyBF,UAiCI,cACA,gBACA,sCX9EF,kBW2CF,UAwCI,cACA,oBACA,cACA,gBACA,iCACA,oBACA,6BAGA,oBACE,oCAIF,gCACE,8CAIF,uCACE,CALA,oCAIF,uCACE,CALA,yCAIF,uCACE,CALA,+BAIF,uCACE,yBAIF,sCACE,8DAIF,gCACE,mBACA,4CACA,8BACA,yFAGA,uCAEE,CALF,+EAGA,uCAEE,CALF,oFAGA,uCAEE,CALF,wJAGA,uCAEE,mBAOR,iBACE,UACA,aACA,cACA,eACA,qCAEE,wBAIF,UACE,gCAIF,SACE,WACA,0CAGA,WACE,aACA,8CAGA,oBACE,0CXzIN,+BW8HA,SAiBI,WACA,0CAGA,WACE,aACA,gDAIF,YACE,sCX5KN,+BWgJA,mBAkCI,+CAGA,YACE,+BAMN,SACE,YACA,sBACA,UACA,wEAEE,oBAEF,wCAGA,aACE,WACA,0CXvLJ,6BW0KA,SAkBI,YACA,wCAGA,aACE,WACA,yHAKJ,kBAEE,UACA,uBACA,CATE,kHAKJ,kBAEE,UACA,uBACA,8HAGA,UACE,CAJF,wHAGA,UACE,oBAOR,iBACE,UACA,WACA,gBACA,8BACA,0CX3NA,mBWsNF,UASI,SACA,sCXlPF,mBWwOF,UAeI,UACA,yBACA,+DAGA,kGV5YJ,UU+YM,yBAMN,WACE,gBACA,4CACA,mCAEA,CAFA,0BAEA,qCACA,CADA,gCACA,CADA,4BACA,mBACA,oEAGA,uBAVF,uBAWI,gEXrSA,uBW0RJ,aAgBI,yCXrRF,uBWqQF,aAqBI,sCX1RF,uBWqQF,YA0BI,qBAEA,gEACA,mEAGA,eACE,8BAIF,qDACE,2CAIF,WACE,aACA,iDAIF,oDACE,uDAGA,0CACE,oBAQV,gCACE,sBACA,yBAGA,eACE,wCACA,iBACA,mBACA,sDACA,wBACA,qCX7UA,wBWuUF,mBAUI,mCAGA,oBACE,qBACA,0BAMN,QACE,UACA,gBACA,yBAIF,4DACE,qCAGA,eACE,yBAKJ,aACE,aACA,4BACA,wBACA,6DAGA,uDAEE,mIAGA,UACE,iDAKJ,mBACE,iCAKJ,aACE,oBACA,gCACA,iBACA,UACA,eACA,8CAEE,wBAEF,6EAGA,+BAEE,wDACA,qCXrZF,gCWqYF,mBAqBI,2CAGA,oBACE,mBACA,0DAKJ,YACE,qCAOA,WACE,4BAMN,iBACE,gBACA,gBACA,qCXtbA,2BWmbF,mBAOI,sCAGA,oBACE,mBACA,gEAQF,eACE,gBACA,gBACA,gBACA,yBAMN,iBACE,OACA,aACA,cACA,aACA,wCACA,gCAGA,oBACE,WACA,YACA,8BACA,gDACA,CADA,uCACA,8BACA,CADA,qBACA,WACA,mCAIF,OACE,aACA,0CAGA,oBACE,0CXzdJ,wBW+bF,YAgCI,2BAKJ,aACE,gBACA,iBACA,gBACA,2BAIF,mBACE,gBACA,cACA,gBACA,wCACA,iBACA,gBACA,uBACA,4BACA,qBACA,0CXtfA,0BW4eF,eAcI,qBACA,gEXliBA,0BWmhBJ,eAoBI,qBACA,iCAIF,4BACE,qDACA,0BAKJ,aACE,iBACA,kBACA,wBAIF,+BACE,6BACA,iDC/rBJ,OACE,6BACE,CADF,qBACE,MAGF,oCACE,CADF,4BACE,EDyrBA,wCC/rBJ,OACE,6BACE,CADF,yBACE,CADF,qBACE,MAGF,oCACE,CADF,gCACE,CADF,4BACE,cASJ,uBACE,CADF,eACE,WACA,sBACA,cACA,iBACA,gBACA,cAGA,YATF,YAUI,2CZiJA,qBY1IA,cACE,MACA,cACA,UACA,cACA,YACA,4CACA,wBACA,yEAEE,gCAIF,cACE,aACA,oEAIF,sGXtCJ,8BWyCM,8EAGA,8BACE,8CAKJ,eACE,yBAMN,YACE,QACA,qCZ+EA,uBYjFF,aAMI,gDAGA,kBACE,0BAMN,eACE,eACA,gBACA,mCAEA,CAFA,0BAEA,qBAEA,gEACA,6BAMA,6DACE,CADF,qDACE,0CZoEF,6CY7DE,iBACE,MACA,QACA,SACA,OACA,SACA,8BACA,CADA,yBACA,CADA,qBACA,gCAKJ,qDACE,4CAIF,WACE,aACA,kDAIF,oDACE,wDAGA,0CACE,2CCjJR,GACE,QACE,MAGF,aACE,ED2II,kCCjJR,GACE,QACE,MAGF,aACE,2CAKJ,GACE,0BACE,UACA,KAGF,SACE,MAGF,wBACE,UACA,EAjBA,iCAKJ,GACE,0BACE,UACA,KAGF,SACE,MAGF,wBACE,UACA,aASJ,aACE,iBACA,gBACA,mBACA,mCAEA,CAFA,0BAEA,yBACA,kBAGA,UACE,kBAIF,oBACE,aACA,cACA,sBACA,sBAGA,gBACE,kBACA,gCAGA,kBACE,oBACA,yCAKJ,iBACE,kBACA,mDAGA,kBACE,oBACA,mBACA,qBACA,wBAMN,oBACE,8BACA,kBACA,gBACA,gBACA,uBACA,sBACA,mBAIF,QACE,UACA,gBACA,gBACA,iBACA,qBACA,YACA,wCAGA,sDACE,CADF,8CACE,kBAKJ,UACE,4BAGA,WACE,uCAIF,sDACE,CADF,8CACE,0BAIF,cACE,YACA,sCAIF,YACE,UCjIN,UACE,cACA,iCACA,4CACA,4BACA,iBAGA,eACE,0CdyKA,SclLJ,YAcI,eAIF,SAlBF,YAmBI,iBAIF,QACE,kBACA,UACA,mBACA,gBACA,gBACA,0BAGA,kBACE,oBACA,gBAKJ,oBACE,cACA,oBACA,mBACA,gBAKF,aACE,iBACA,gBACA,WACA,wEAEE,uBAIF,eACE,6CAIF,aAEE,UACA,4CAKA,qBACE,4CADF,qBACE,4CADF,qBACE,4CADF,qBACE,4CADF,sBACE,4CADF,sBACE,4CADF,sBACE,4CADF,sBACE,6CADF,sBACE,6CADF,sBACE,6CADF,sBACE,6CADF,sBACE,6CADF,sBACE,6CADF,sBACE,6CADF,sBACE,gCAMN,mBACE,+CAIA,yBACE,UACA,yDAEE,wCdyEJ,uEc/DA,YACE,2DAUE,aACE,gBACA,oBACA,wBACA,yEAGA,YACE,wEAKJ,YACE,gFAGA,aACE,UACA,8FAGA,YACE,sDAOR,aAGE,kFAGA,eACE,6FAGA,oBACE,2GAGA,gBACE,6EAMN,YACE,QC1IV,4RAMI,qwHAUF,iBACE,gBACA,gBACA,oCACA,iBACA,wBACA,+CACA,gCACA,oBACA,mEAEE,iEAIF,gCACE,iBACA,cAIF,4CArBF,eAsBI,gFAIF,mBACE,iIAIF,YACE,6FAIF,kBACE,mFAIF,eACE,2FAIF,YACE,mDAKJ,iBACE,2BACA,iCACA,gBACA,qCACA,uEAGA,0BACE,+BACA,mFAIF,eACE,mEAIF,iBACE,WACA,WACA,YACA,yBCwIU,mDDtIV,CCsIU,0CDtIV,8BACA,CADA,qBACA,WACA,uFAGA,WACE,aACA,6DAKJ,cACE,gBACA,mBACA,6BACA,sBACA,gBACA,iGAIF,YACE,uDAcJ,oBAHO,+DAQP,oCACE,+EAGA,wBAZK,mDAcH,CAdG,0CAcH,8BACA,CADA,qBACA,iLAZJ,oBAHO,yMAQP,mCACE,yPAGA,wBAZK,uDAcH,CAdG,8CAcH,8BACA,CADA,qBACA,6GAZJ,oBAHO,6HAQP,mCACE,6JAGA,wBAZK,mDAcH,CAdG,0CAcH,8BACA,CADA,qBACA,2KAZJ,oBAHO,mMAQP,mCACE,mPAGA,wBAZK,kDAcH,CAdG,yCAcH,8BACA,CADA,qBACA,2KAZJ,oBAHO,mMAQP,kCACE,mPAGA,wBAZK,sDAcH,CAdG,6CAcH,8BACA,CADA,qBACA,yKAZJ,oBAHO,iMAQP,oCACE,iPAGA,wBAZK,uDAcH,CAdG,8CAcH,8BACA,CADA,qBACA,yLAZJ,oBAHO,iNAQP,mCACE,iQAGA,wBAZK,sDAcH,CAdG,6CAcH,8BACA,CADA,qBACA,+KAZJ,oBAHO,uMAQP,mCACE,uPAGA,wBAZK,sDAcH,CAdG,6CAcH,8BACA,CADA,qBACA,mHAZJ,oBAHO,mIAQP,mCACE,mKAGA,wBAZK,qDAcH,CAdG,4CAcH,8BACA,CADA,qBACA,qDAZJ,oBAHO,6DAQP,kCACE,6EAGA,wBAZK,kDAcH,CAdG,yCAcH,8BACA,CADA,qBACA,6DAZJ,oBAHO,qEAQP,oCACE,qFAGA,wBAZK,sDAcH,CAdG,6CAcH,8BACA,CADA,qBACA,+GAZJ,oBAHO,+HAQP,qCACE,+JAGA,wBAZK,oDAcH,CAdG,2CAcH,8BACA,CADA,qBACA,6DElKJ,sCAEE,8BAGF,yCACE,sRAGF,oCASE,4JAGF,qCAKE,yLAGF,oCAME,yLAGF,qCAME,6DAGF,kCAEE,8FAGF,sCAGE,6HAGF,sCAIE,6HAGF,qCAIE,yLAGF,qCAME,4JAGF,sCAKE,yPAGF,qCAQE,+DAGF,iBAEE,iBACA,oBACA,gCAGF,4CACE,gCAGF,4CACE,kCAIF,aACE,yBACA,yBACA,yCACA,kCASJ,aACE,gBACA,qFAIA,aAEE,UACA,wCAKF,YACE,0CAKF,QACE,oDAKF,8BACE,gBACA,gBACA,yCACA,yBACA,CADA,qBACA,CADA,oBACA,CADA,gBACA,wDAIF,2BACE,gEACA,gEAGA,uCACE,iBACA,8CAMJ,MACE,gBACA,0DAQF,YACE,cACA,oBACA,oEAGA,eACE,0CjBlBF,+CiB0BA,kBACE,0DAGA,gBACE,gBACA,0DAIF,eACE,0DAKJ,kBACE,gBACA,oEAGA,gBACE,gBACA,QCnOR,yMACE,4BASA,oBACE,mCAGA,kBACE,mBACA,oBACA,0BACA,iCAQF,YACE,SACA,WACA,gCAIF,yBACE,wCAIF,aACE,mBACA,mBACA,oBACA,uBAKJ,uCACE,iBACA,0BAGA,aACE,0BAIF,sBACE,iCAGA,gCACE,uCAIF,YACE,oGAIF,uBAEE,UACA,wDAIF,+BACE,2BAMN,oBACE,uBACA,+BAIF,oBACE,gCACA,YAEA,2BACA,8BACA,UACA,iEAEE,yCAKF,8BACE,uCAIF,oBACE,YACA,aACA,8BACA,4CACA,CADA,mCACA,8BACA,CADA,qBACA,WACA,qDAME,oBACE,cAMN,8BAvCF,+BAwCI,wBACA,UACA,0BClIJ,oBACE,kBACA,kBAGA,UACA,0DAEE,mCAKF,kBACE,oBACA,mCAIF,yCACE,cAIF,wBAxBF,YAyBI,+FAKJ,kBAGE,UACA,oDAEE,6FAMJ,+BAGE,qBAMF,wBACE,+EAYE,yBACE,kFAIF,aACE,mBACA,kBACA,WACA,uGAIF,kBACE,mBACA,2BAfF,yBACE,4BAIF,aACE,oBACA,mBACA,WACA,mCAIF,mBACE,oBACA,qDAfF,yBACE,uDAIF,aACE,mBACA,kBACA,WACA,qEAIF,kBACE,mBACA,4BC/EN,iBACE,0CpB8KA,2BoB/KF,gBAKI,+BAIF,yBACE,CADF,sBACE,CADF,iBACE,2BACA,gBACA,cACA,kBACA,2ECdJ,kCAGE,CAHF,0BAGE,wBAIF,4CACE,wBAIF,4CACE,6BAIF,qCACE,qCAGA,aACE,oCAIF,aACE,2BAKJ,aACE,aACA,oBACA,mBACA,cACA,gBACA,wCAGA,eACE,uCAIF,kBACE,OClDN,+LACE,qBASA,aAGE,cACA,iBACA,0CAGA,uBACE,iCAIF,gBACE,yCAIA,mBACE,4BAKJ,aACE,WACA,qBAKJ,aAGE,gBACA,kCACA,6BACA,8BACA,eACA,yCAGA,YACE,wCACA,+BAIF,iCACE,6CAIF,YACE,4BAIF,iBACE,UACA,YACA,WACA,YACA,8BACA,0CACA,CADA,iCACA,8BACA,CADA,qBACA,uBACA,2BACA,WACA,sCAGA,aACE,WACA,yBACA,yEClFN,aAGE,gBACA,oBACA,0BAIF,oBACE,eACA,wBACA,8BAGA,aACE,gBACA,kBACA,mCCfJ,uBACE,CADF,eACE,qBACA,WACA,4BACA,4BACA,4BACA,wCACA,yCACA,gEACA,2BACA,yBACA,CADA,qBACA,CADA,oBACA,CADA,gBACA,4DCdF,iBAEE,SACA,cACA,gCACA,+BACA,wBAIF,cACE,wCACA,oCAqDE,kBACE,YAlDgB,yCAiDlB,kBACE,YAlDgB,0CAiDlB,kBACE,YAlDgB,wCAiDlB,kBACE,YAlDgB,6CAiDlB,kBACE,YAlDgB,8CAiDlB,kBACE,YAlDgB,wCAiDlB,kBACE,YAlDgB,6CAiDlB,kBACE,YAlDgB,8CAiDlB,kBACE,YAlDgB,qCAiDlB,kBACE,YAlDgB,0CAiDlB,kBACE,YAlDgB,2CAiDlB,kBACE,YAlDgB,uCAiDlB,kBACE,YAlDgB,4CAiDlB,kBACE,YAlDgB,6CAiDlB,kBACE,YAlDgB,sCAiDlB,kBACE,YAlDgB,2CAiDlB,kBACE,YAlDgB,4CAiDlB,kBACE,YAlDgB,sCAiDlB,kBACE,YAlDgB,2CAiDlB,kBACE,YAlDgB,4CAiDlB,kBACE,YAlDgB,wCAiDlB,kBACE,YAlDgB,6CAiDlB,kBACE,YAlDgB,8CAiDlB,kBACE,YAlDgB,2CAiDlB,kBACE,YAlDgB,2CAiDlB,kBACE,YAlDgB,4CAiDlB,kBACE,YAlDgB,yCAiDlB,kBACE,YAlDgB,0CAiDlB,kBACE,YAlDgB,wCAiDlB,kBACE,YAlDgB,0CAiDlB,kBACE,YAlDgB,sCAiDlB,kBACE,YAlDgB,6CAiDlB,kBACE,YAlDgB,uCAiDlB,kBACE,YAlDgB,sCAiDlB,kBACE,YAlDgB,oCAiDlB,kBACE,YAlDgB,uCAiDlB,kBACE,YAlDgB,qCAiDlB,kBACE,YAlDgB,uCAiDlB,kBACE,YAlDgB,0CAiDlB,kBACE,YAlDgB,wCAiDlB,kBACE,YAlDgB,6CAiDlB,kBACE,YAlDgB,mCA+DlB,iBACE,YAPgB,yCAMlB,iBACE,YAPgB,qCAMlB,iBACE,YAPgB,6BCzEtB,YACE,SACA,WACA,2DACA,wQAGA,QAKE,iSAGA,wBACE,0BACA,yCAKJ,QACE,yBAKJ,iBACE,aACA,eACA,aACA,oBACA,+BAGA,iBACE,QACA,SACA,UACA,6CAGA,+BACE,uCACA,6DAGA,aACE,2CAKJ,kBACE,yDAIF,YACE,wCACA,+BAKJ,SACE,WACA,gCACA,wCACA,gBACA,iBACA,sCACA,eACA,uBACA,0CAGA,+BACE,OClFR,kVACE,4VAGA,6BAWA,iBACE,qBACA,6CAIA,iBACE,UACA,UACA,uDAGA,UACE,aACA,6DASJ,iBACE,UACA,YACA,aACA,cACA,sDACA,2CACA,CADA,kCACA,8BACA,CADA,qBACA,WACA,uEAGA,YACE,aACA,qFAKJ,wBXiWa,oDW/VX,CX+VW,2CW/VX,gDAIF,UACE,UACA,C","file":"assets/stylesheets/main.38780c08.min.css","sourcesContent":["html{box-sizing:border-box;text-size-adjust:none}*,*::before,*::after{box-sizing:inherit}body{margin:0}hr{box-sizing:content-box;overflow:visible}a,button,label,input{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:separate;border-spacing:0}td,th{font-weight:normal;vertical-align:top}button{margin:0;padding:0;font-size:inherit;background:transparent;border:0}input{border:0;outline:none}:root{--md-default-fg-color: hsla(0, 0%, 0%, 0.87);--md-default-fg-color--light: hsla(0, 0%, 0%, 0.54);--md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32);--md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07);--md-default-bg-color: hsla(0, 0%, 100%, 1);--md-default-bg-color--light: hsla(0, 0%, 100%, 0.7);--md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3);--md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12);--md-primary-fg-color: hsla(231, 48%, 48%, 1);--md-primary-fg-color--light: hsla(230, 44%, 64%, 1);--md-primary-fg-color--dark: hsla(232, 54%, 41%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);--md-accent-fg-color: hsla(231, 99%, 66%, 1);--md-accent-fg-color--transparent: hsla(231, 99%, 66%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}:root>*{--md-code-fg-color: hsla(200, 18%, 26%, 1);--md-code-bg-color: hsla(0, 0%, 96%, 1);--md-code-hl-color: hsla(60, 100%, 50%, 0.5);--md-code-hl-number-color: hsla(0, 67%, 50%, 1);--md-code-hl-special-color: hsla(340, 83%, 47%, 1);--md-code-hl-function-color: hsla(291, 45%, 50%, 1);--md-code-hl-constant-color: hsla(250, 63%, 60%, 1);--md-code-hl-keyword-color: hsla(219, 54%, 51%, 1);--md-code-hl-string-color: hsla(150, 63%, 30%, 1);--md-code-hl-name-color: var(--md-code-fg-color);--md-code-hl-operator-color: var(--md-default-fg-color--light);--md-code-hl-punctuation-color: var(--md-default-fg-color--light);--md-code-hl-comment-color: var(--md-default-fg-color--light);--md-code-hl-generic-color: var(--md-default-fg-color--light);--md-code-hl-variable-color: var(--md-default-fg-color--light);--md-typeset-color: var(--md-default-fg-color);--md-typeset-a-color: var(--md-primary-fg-color);--md-typeset-mark-color: hsla(60, 100%, 50%, 0.5);--md-typeset-del-color: hsla(6, 90%, 60%, 0.15);--md-typeset-ins-color: hsla(150, 90%, 44%, 0.15);--md-typeset-kbd-color: hsla(0, 0%, 98%, 1);--md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1);--md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1);--md-admonition-fg-color: var(--md-default-fg-color);--md-admonition-bg-color: var(--md-default-bg-color);--md-footer-fg-color: hsla(0, 0%, 100%, 1);--md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7);--md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.3);--md-footer-bg-color: hsla(0, 0%, 0%, 0.87);--md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32)}.md-icon svg{display:block;width:1.2rem;height:1.2rem;margin:0 auto;fill:currentColor}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body,input{color:var(--md-typeset-color);font-feature-settings:\"kern\",\"liga\";font-family:-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif}code,pre,kbd{color:var(--md-typeset-color);font-feature-settings:\"kern\";font-family:SFMono-Regular,Consolas,Menlo,monospace}:root{--md-typeset-table--ascending: svg-load(\"@mdi/svg/svg/arrow-down.svg\");--md-typeset-table--descending: svg-load(\"@mdi/svg/svg/arrow-up.svg\")}.md-typeset{font-size:.8rem;line-height:1.6;color-adjust:exact}@media print{.md-typeset{font-size:.68rem}}.md-typeset p,.md-typeset ul,.md-typeset ol,.md-typeset blockquote{margin:1em 0}.md-typeset h1{margin:0 0 1.25em;color:var(--md-default-fg-color--light);font-weight:300;font-size:2em;line-height:1.3;letter-spacing:-0.01em}.md-typeset h2{margin:1.6em 0 .64em;font-weight:300;font-size:1.5625em;line-height:1.4;letter-spacing:-0.01em}.md-typeset h3{margin:1.6em 0 .8em;font-weight:400;font-size:1.25em;line-height:1.5;letter-spacing:-0.01em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{margin:1em 0;font-weight:700;letter-spacing:-0.01em}.md-typeset h5,.md-typeset h6{margin:1.25em 0;color:var(--md-default-fg-color--light);font-weight:700;font-size:.8em;letter-spacing:-0.01em}.md-typeset h5{text-transform:uppercase}.md-typeset hr{margin:1.5em 0;border-bottom:.05rem dotted var(--md-default-fg-color--lighter)}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a::before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset code,.md-typeset pre,.md-typeset kbd{color:var(--md-code-fg-color);direction:ltr}@media print{.md-typeset code,.md-typeset pre,.md-typeset kbd{white-space:pre-wrap}}.md-typeset code{padding:0 .2941176471em;font-size:.85em;word-break:break-word;background-color:var(--md-code-bg-color);border-radius:.1rem;box-decoration-break:clone}.md-typeset h1 code,.md-typeset h2 code,.md-typeset h3 code,.md-typeset h4 code,.md-typeset h5 code,.md-typeset h6 code{margin:initial;padding:initial;background-color:transparent;box-shadow:none}.md-typeset a>code{color:currentColor}.md-typeset pre{position:relative;margin:1em 0;line-height:1.4}.md-typeset pre>code{display:block;margin:0;padding:.7720588235em 1.1764705882em;overflow:auto;word-break:normal;box-shadow:none;box-decoration-break:slice;touch-action:auto;scrollbar-width:thin;scrollbar-color:var(--md-default-fg-color--lighter) transparent}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-typeset pre>code::-webkit-scrollbar{width:.2rem;height:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@media screen and (max-width: 44.9375em){.md-typeset>pre{margin:1em -0.8rem}.md-typeset>pre code{border-radius:0}}.md-typeset kbd{display:inline-block;padding:0 .6666666667em;color:var(--md-default-fg-color);font-size:.75em;vertical-align:text-top;word-break:break-word;background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -0.1rem .2rem var(--md-typeset-kbd-accent-color) inset}.md-typeset mark{color:inherit;word-break:break-word;background-color:var(--md-typeset-mark-color);box-decoration-break:clone}.md-typeset abbr{text-decoration:none;border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help}@media(hover: none){.md-typeset abbr{position:relative}.md-typeset abbr[title]:focus::after,.md-typeset abbr[title]:hover::after{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);position:absolute;left:0;display:inline-block;width:auto;min-width:max-content;max-width:80%;margin-top:2em;padding:.2rem .3rem;color:var(--md-default-bg-color);font-size:.7rem;background:var(--md-default-fg-color);border-radius:.1rem;content:attr(title)}}.md-typeset small{opacity:.75}.md-typeset sup,.md-typeset sub{margin-left:.078125em}[dir=rtl] .md-typeset sup,[dir=rtl] .md-typeset sub{margin-right:.078125em;margin-left:initial}.md-typeset blockquote{padding-left:.6rem;color:var(--md-default-fg-color--light);border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{padding-right:.6rem;padding-left:initial;border-right:.2rem solid var(--md-default-fg-color--lighter);border-left:initial}.md-typeset ul{list-style-type:disc}.md-typeset ul,.md-typeset ol{margin-left:.625em;padding:0}[dir=rtl] .md-typeset ul,[dir=rtl] .md-typeset ol{margin-right:.625em;margin-left:initial}.md-typeset ul ol,.md-typeset ol ol{list-style-type:lower-alpha}.md-typeset ul ol ol,.md-typeset ol ol ol{list-style-type:lower-roman}.md-typeset ul li,.md-typeset ol li{margin-bottom:.5em;margin-left:1.25em}[dir=rtl] .md-typeset ul li,[dir=rtl] .md-typeset ol li{margin-right:1.25em;margin-left:initial}.md-typeset ul li p,.md-typeset ul li blockquote,.md-typeset ol li p,.md-typeset ol li blockquote{margin:.5em 0}.md-typeset ul li:last-child,.md-typeset ol li:last-child{margin-bottom:0}.md-typeset ul li ul,.md-typeset ul li ol,.md-typeset ol li ul,.md-typeset ol li ol{margin:.5em 0 .5em .625em}[dir=rtl] .md-typeset ul li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ol li ol{margin-right:.625em;margin-left:initial}.md-typeset dd{margin:1em 0 1.5em 1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em;margin-left:initial}.md-typeset img,.md-typeset svg{max-width:100%;height:auto}.md-typeset img[align=left],.md-typeset svg[align=left]{margin:1em;margin-left:0}.md-typeset img[align=right],.md-typeset svg[align=right]{margin:1em;margin-right:0}.md-typeset img[align]:only-child,.md-typeset svg[align]:only-child{margin-top:0}.md-typeset figure{width:fit-content;max-width:100%;margin:0 auto;text-align:center}.md-typeset figcaption{max-width:24rem;margin:.5em auto 2em;font-style:italic}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){display:inline-block;max-width:100%;overflow:auto;font-size:.64rem;background:var(--md-default-bg-color);border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1);touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) th>*:first-child,.md-typeset table:not([class]) td>*:first-child{margin-top:0}.md-typeset table:not([class]) th>*:last-child,.md-typeset table:not([class]) td>*:last-child{margin-bottom:0}.md-typeset table:not([class]) th:not([align]),.md-typeset table:not([class]) td:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) th:not([align]),[dir=rtl] .md-typeset table:not([class]) td:not([align]){text-align:right}.md-typeset table:not([class]) th{min-width:5rem;padding:.9375em 1.25em;color:var(--md-default-bg-color);vertical-align:top;background-color:var(--md-default-fg-color--light)}.md-typeset table:not([class]) th a{color:inherit}.md-typeset table:not([class]) td{padding:.9375em 1.25em;vertical-align:top;border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-typeset table:not([class]) tr{transition:background-color 125ms}.md-typeset table:not([class]) tr:hover{background-color:rgba(0,0,0,.035);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) tr:first-child td{border-top:0}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}.md-typeset table th[role=columnheader]::after{display:inline-block;width:1.2em;height:1.2em;margin-left:.5em;vertical-align:sub;mask-repeat:no-repeat;content:\" \"}.md-typeset table th[role=columnheader][aria-sort=ascending]::after{background-color:currentColor;mask-image:var(--md-typeset-table--ascending)}.md-typeset table th[role=columnheader][aria-sort=descending]::after{background-color:currentColor;mask-image:var(--md-typeset-table--descending)}.md-typeset__scrollwrap{margin:1em -0.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;width:100%;margin:0;overflow:hidden}html{height:100%;overflow-x:hidden;font-size:125%}@media screen and (min-width: 100em){html{font-size:137.5%}}@media screen and (min-width: 125em){html{font-size:150%}}body{position:relative;display:flex;flex-direction:column;width:100%;min-height:100%;font-size:.5rem;background-color:var(--md-default-bg-color)}@media screen and (max-width: 59.9375em){body[data-md-state=lock]{position:fixed}}@media print{body{display:block}}hr{display:block;height:.05rem;padding:0;border:0}.md-grid{max-width:61rem;margin-right:auto;margin-left:auto}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{display:block;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}.md-toggle{display:none}.md-overlay{position:fixed;top:0;z-index:3;width:0;height:0;background-color:rgba(0,0,0,.54);opacity:0;transition:width 0ms 250ms,height 0ms 250ms,opacity 250ms}@media screen and (max-width: 76.1875em){[data-md-toggle=drawer]:checked~.md-overlay{width:100%;height:100%;opacity:1;transition:width 0ms,height 0ms,opacity 250ms}}.md-skip{position:fixed;z-index:-1;margin:.5rem;padding:.3rem .5rem;color:var(--md-default-bg-color);font-size:.64rem;background-color:var(--md-default-fg-color);border-radius:.1rem;transform:translateY(0.4rem);opacity:0}.md-skip:focus{z-index:10;transform:translateY(0);opacity:1;transition:transform 250ms cubic-bezier(0.4, 0, 0.2, 1),opacity 175ms 75ms}@page{margin:25mm}.md-announce{overflow:auto;background-color:var(--md-footer-bg-color)}.md-announce__inner{margin:.6rem auto;padding:0 .8rem;color:var(--md-footer-fg-color);font-size:.7rem}@media print{.md-announce{display:none}}.md-typeset .md-button{display:inline-block;padding:.625em 2em;color:var(--md-primary-fg-color);font-weight:700;border:.1rem solid currentColor;border-radius:.1rem;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{color:var(--md-accent-bg-color);background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color)}:root{--md-clipboard-icon: svg-load(\"@mdi/svg/svg/content-copy.svg\")}.md-clipboard{position:absolute;top:.5em;right:.5em;z-index:1;width:1.5em;height:1.5em;color:var(--md-default-fg-color--lightest);border-radius:.1rem;cursor:pointer;transition:color 125ms}@media print{.md-clipboard{display:none}}.md-clipboard::after{display:block;width:1.125em;height:1.125em;margin:0 auto;background-color:currentColor;mask-image:var(--md-clipboard-icon);mask-repeat:no-repeat;content:\"\"}pre:hover .md-clipboard{color:var(--md-default-fg-color--light)}pre .md-clipboard:focus,pre .md-clipboard:hover{color:var(--md-accent-fg-color)}.md-content{flex:1;max-width:100%}@media screen and (min-width: 60em)and (max-width: 76.1875em){.md-content{max-width:calc(100% - 12.1rem)}}@media screen and (min-width: 76.25em){.md-content{max-width:calc(100% - 12.1rem * 2)}}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width: 76.25em){.md-content__inner{margin-right:1.2rem;margin-left:1.2rem}}.md-content__inner::before{display:block;height:.4rem;content:\"\"}.md-content__inner>:last-child{margin-bottom:0}.md-content__button{float:right;margin:.4rem 0;margin-left:.4rem;padding:0}[dir=rtl] .md-content__button{float:left;margin-right:.4rem;margin-left:initial}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}@media print{.md-content__button{display:none}}.md-dialog{box-shadow:0 2px 2px 0 rgba(0,0,0,.14),0 1px 5px 0 rgba(0,0,0,.12),0 3px 1px -2px rgba(0,0,0,.2);position:fixed;right:.8rem;bottom:.8rem;left:initial;z-index:2;display:block;min-width:11.1rem;padding:.4rem .6rem;color:var(--md-default-bg-color);font-size:.7rem;background:var(--md-default-fg-color);border:none;border-radius:.1rem;transform:translateY(100%);opacity:0;transition:transform 0ms 400ms,opacity 400ms}[dir=rtl] .md-dialog{right:initial;left:.8rem}.md-dialog[data-md-state=open]{transform:translateY(0);opacity:1;transition:transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1),opacity 400ms}@media print{.md-dialog{display:none}}.md-header{position:sticky;top:0;right:0;left:0;z-index:2;height:2.4rem;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem rgba(0,0,0,0),0 .2rem .4rem rgba(0,0,0,0);transition:color 250ms,background-color 250ms}.no-js .md-header{box-shadow:none;transition:none}.md-header[data-md-state=shadow]{box-shadow:0 0 .2rem rgba(0,0,0,.1),0 .2rem .4rem rgba(0,0,0,.2);transition:color 250ms,background-color 250ms,box-shadow 250ms}@media print{.md-header{display:none}}.md-header-nav{display:flex;padding:0 .2rem}.md-header-nav__button{position:relative;z-index:1;display:block;margin:.2rem;padding:.4rem;cursor:pointer;transition:opacity 250ms}[dir=rtl] .md-header-nav__button svg{transform:scaleX(-1)}.md-header-nav__button:focus,.md-header-nav__button:hover{opacity:.7}.md-header-nav__button.md-logo{margin:.2rem;padding:.4rem}.md-header-nav__button.md-logo img,.md-header-nav__button.md-logo svg{display:block;width:1.2rem;height:1.2rem;fill:currentColor}.no-js .md-header-nav__button[for=__search]{display:none}@media screen and (min-width: 60em){.md-header-nav__button[for=__search]{display:none}}@media screen and (max-width: 76.1875em){.md-header-nav__button.md-logo{display:none}}@media screen and (min-width: 76.25em){.md-header-nav__button[for=__drawer]{display:none}}.md-header-nav__topic{position:absolute;width:100%;transition:transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms}.md-header-nav__topic+.md-header-nav__topic{z-index:-1;transform:translateX(1.25rem);opacity:0;transition:transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),opacity 150ms;pointer-events:none}[dir=rtl] .md-header-nav__topic+.md-header-nav__topic{transform:translateX(-1.25rem)}.no-js .md-header-nav__topic{position:initial}.no-js .md-header-nav__topic+.md-header-nav__topic{display:none}.md-header-nav__title{flex-grow:1;padding:0 1rem;font-size:.9rem;line-height:2.4rem}.md-header-nav__title[data-md-state=active] .md-header-nav__topic{z-index:-1;transform:translateX(-1.25rem);opacity:0;transition:transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),opacity 150ms;pointer-events:none}[dir=rtl] .md-header-nav__title[data-md-state=active] .md-header-nav__topic{transform:translateX(1.25rem)}.md-header-nav__title[data-md-state=active] .md-header-nav__topic+.md-header-nav__topic{z-index:0;transform:translateX(0);opacity:1;transition:transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms;pointer-events:initial}.md-header-nav__title>.md-header-nav__ellipsis{position:relative;width:100%;height:100%}.md-header-nav__source{display:none}@media screen and (min-width: 60em){.md-header-nav__source{display:block;width:11.7rem;max-width:11.7rem;margin-left:1rem}[dir=rtl] .md-header-nav__source{margin-right:1rem;margin-left:initial}}@media screen and (min-width: 76.25em){.md-header-nav__source{margin-left:1.4rem}[dir=rtl] .md-header-nav__source{margin-right:1.4rem}}.md-footer{color:var(--md-footer-fg-color);background-color:var(--md-footer-bg-color)}@media print{.md-footer{display:none}}.md-footer-nav__inner{padding:.2rem;overflow:auto}.md-footer-nav__link{display:flex;padding-top:1.4rem;padding-bottom:.4rem;transition:opacity 250ms}@media screen and (min-width: 45em){.md-footer-nav__link{width:50%}}.md-footer-nav__link:focus,.md-footer-nav__link:hover{opacity:.7}.md-footer-nav__link--prev{float:left}[dir=rtl] .md-footer-nav__link--prev{float:right}[dir=rtl] .md-footer-nav__link--prev svg{transform:scaleX(-1)}@media screen and (max-width: 44.9375em){.md-footer-nav__link--prev{width:25%}.md-footer-nav__link--prev .md-footer-nav__title{display:none}}.md-footer-nav__link--next{float:right;text-align:right}[dir=rtl] .md-footer-nav__link--next{float:left;text-align:left}[dir=rtl] .md-footer-nav__link--next svg{transform:scaleX(-1)}@media screen and (max-width: 44.9375em){.md-footer-nav__link--next{width:75%}}.md-footer-nav__title{position:relative;flex-grow:1;max-width:calc(100% - 2.4rem);padding:0 1rem;font-size:.9rem;line-height:2.4rem}.md-footer-nav__button{margin:.2rem;padding:.4rem}.md-footer-nav__direction{position:absolute;right:0;left:0;margin-top:-1rem;padding:0 1rem;font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-footer-copyright{width:100%;margin:auto .6rem;padding:.4rem 0;color:var(--md-footer-fg-color--lighter);font-size:.64rem}@media screen and (min-width: 45em){.md-footer-copyright{width:auto}}.md-footer-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-footer-social{margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width: 45em){.md-footer-social{padding:.6rem 0}}.md-footer-social__link{display:inline-block;width:1.6rem;height:1.6rem;text-align:center}.md-footer-social__link::before{line-height:1.9}.md-footer-social__link svg{max-height:.8rem;vertical-align:-25%;fill:currentColor}:root{--md-nav-icon--prev: svg-load(\"@mdi/svg/svg/arrow-left.svg\");--md-nav-icon--next: svg-load(\"@mdi/svg/svg/chevron-right.svg\");--md-toc-icon: svg-load(\"@mdi/svg/svg/table-of-contents.svg\")}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{display:block;padding:0 .6rem;overflow:hidden;font-weight:700;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{width:100%;height:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{display:block;width:2.4rem;height:2.4rem}.md-nav__title .md-nav__button.md-logo svg{fill:currentColor}.md-nav__list{margin:0;padding:0;list-style:none}.md-nav__item{padding:0 .6rem}.md-nav__item:last-child{padding-bottom:.6rem}.md-nav__item .md-nav__item{padding-right:0}[dir=rtl] .md-nav__item .md-nav__item{padding-right:.6rem;padding-left:0}.md-nav__item .md-nav__item:last-child{padding-bottom:0}.md-nav__link{display:block;margin-top:.625em;overflow:hidden;text-overflow:ellipsis;cursor:pointer;transition:color 125ms;scroll-snap-align:start}html .md-nav__link[for=__toc]{display:none}html .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__link[data-md-state=blur]{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active{color:var(--md-typeset-a-color)}.md-nav__item--nested>.md-nav__link{color:inherit}.md-nav__link:focus,.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav__source{display:none}@media screen and (max-width: 76.1875em){.md-nav{background-color:var(--md-default-bg-color)}.md-nav--primary,.md-nav--primary .md-nav{position:absolute;top:0;right:0;left:0;z-index:1;display:flex;flex-direction:column;height:100%}.md-nav--primary .md-nav__title,.md-nav--primary .md-nav__item{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{position:relative;height:5.6rem;padding:3rem .8rem .2rem;color:var(--md-default-fg-color--light);font-weight:400;line-height:2.4rem;white-space:nowrap;background-color:var(--md-default-fg-color--lightest);cursor:pointer}.md-nav--primary .md-nav__title .md-nav__icon{position:absolute;top:.4rem;left:.4rem;display:block;width:1.2rem;height:1.2rem;margin:.2rem}.md-nav--primary .md-nav__title .md-nav__icon::after{display:block;width:100%;height:100%;background-color:currentColor;mask-image:var(--md-nav-icon--prev);mask-repeat:no-repeat;content:\"\"}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem;left:initial}.md-nav--primary .md-nav__title~.md-nav__list{overflow-y:auto;background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>.md-nav__item:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{position:relative;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color)}.md-nav--primary .md-nav__title[for=__drawer] .md-nav__button{position:absolute;top:.2rem;left:.2rem;display:block;margin:.2rem;padding:.4rem;font-size:2.4rem}html [dir=rtl] .md-nav--primary .md-nav__title[for=__drawer] .md-nav__button{right:.2rem;left:initial}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{padding:0;border-top:.05rem solid var(--md-default-fg-color--lightest)}[dir=rtl] .md-nav--primary .md-nav__item{padding:0}.md-nav--primary .md-nav__item--nested>.md-nav__link{padding-right:2.4rem}[dir=rtl] .md-nav--primary .md-nav__item--nested>.md-nav__link{padding-right:.8rem;padding-left:2.4rem}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{position:relative;margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link .md-nav__icon{position:absolute;top:50%;right:.6rem;width:1.2rem;height:1.2rem;margin-top:-0.6rem;color:inherit;font-size:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon::after{display:block;width:100%;height:100%;background-color:currentColor;mask-image:var(--md-nav-icon--next);mask-repeat:no-repeat;content:\"\"}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{right:initial;left:.6rem}[dir=rtl] .md-nav--primary .md-nav__icon::after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav__link{position:static}.md-nav--primary .md-nav--secondary .md-nav{position:static;background-color:transparent}.md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem;padding-left:initial}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem;padding-left:initial}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem;padding-left:initial}.md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem;padding-left:initial}.md-nav__toggle~.md-nav{display:flex;transform:translateX(100%);opacity:0;transition:transform 250ms cubic-bezier(0.8, 0, 0.6, 1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{transform:translateX(0);opacity:1;transition:transform 250ms cubic-bezier(0.4, 0, 0.2, 1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{backface-visibility:hidden}}@media screen and (max-width: 59.9375em){html .md-nav__link[for=__toc]{display:block;padding-right:2.4rem}html .md-nav__link[for=__toc]+.md-nav__link{display:none}html .md-nav__link[for=__toc] .md-icon::after{display:block;width:100%;height:100%;mask-image:var(--md-toc-icon);background-color:currentColor;content:\"\"}html .md-nav__link[for=__toc]~.md-nav{display:flex}html [dir=rtl] .md-nav__link{padding-right:.8rem;padding-left:2.4rem}.md-nav__source{display:block;padding:0 .2rem;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color--dark)}}@media screen and (min-width: 60em){.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}}@media screen and (min-width: 76.25em){.md-nav{transition:max-height 250ms cubic-bezier(0.86, 0, 0.07, 1)}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}.md-nav__toggle~.md-nav{display:none}.md-nav__toggle:checked~.md-nav{display:block}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__icon{float:right;width:.9rem;height:.9rem;transition:transform 250ms}[dir=rtl] .md-nav__icon{float:left;transform:rotate(180deg)}.md-nav__icon::after{display:inline-block;width:100%;height:100%;vertical-align:-0.1rem;background-color:currentColor;mask-image:var(--md-nav-icon--next);mask-repeat:no-repeat;content:\"\"}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon{transform:rotate(90deg)}}:root{--md-search-result-icon: svg-load(\"@mdi/svg/svg/file-search-outline.svg\")}.md-search{position:relative}.no-js .md-search{display:none}@media screen and (min-width: 60em){.md-search{padding:.2rem 0}}.md-search__overlay{z-index:1;opacity:0}@media screen and (max-width: 59.9375em){.md-search__overlay{position:absolute;top:.2rem;left:-2.2rem;width:2rem;height:2rem;overflow:hidden;background-color:var(--md-default-bg-color);border-radius:1rem;transform-origin:center;transition:transform 300ms 100ms,opacity 200ms 200ms;pointer-events:none}[dir=rtl] .md-search__overlay{right:-2.2rem;left:initial}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform 400ms,opacity 100ms}}@media screen and (max-width: 29.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width: 30em)and (max-width: 44.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width: 45em)and (max-width: 59.9375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}@media screen and (min-width: 60em){.md-search__overlay{position:fixed;top:0;left:0;width:0;height:0;background-color:rgba(0,0,0,.54);cursor:pointer;transition:width 0ms 250ms,height 0ms 250ms,opacity 250ms}[dir=rtl] .md-search__overlay{right:0;left:initial}[data-md-toggle=search]:checked~.md-header .md-search__overlay{width:100%;height:100%;opacity:1;transition:width 0ms,height 0ms,opacity 250ms}}.md-search__inner{backface-visibility:hidden}@media screen and (max-width: 59.9375em){.md-search__inner{position:fixed;top:0;left:100%;z-index:2;width:100%;height:100%;transform:translateX(5%);opacity:0;transition:right 0ms 300ms,left 0ms 300ms,transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1),opacity 150ms 150ms}[data-md-toggle=search]:checked~.md-header .md-search__inner{left:0;transform:translateX(0);opacity:1;transition:right 0ms 0ms,left 0ms 0ms,transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms 150ms}[dir=rtl] [data-md-toggle=search]:checked~.md-header .md-search__inner{right:0;left:initial}html [dir=rtl] .md-search__inner{right:100%;left:initial;transform:translateX(-5%)}}@media screen and (min-width: 60em){.md-search__inner{position:relative;float:right;width:11.7rem;padding:.1rem 0;transition:width 250ms cubic-bezier(0.1, 0.7, 0.1, 1)}[dir=rtl] .md-search__inner{float:left}}@media screen and (min-width: 60em)and (max-width: 76.1875em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width: 76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{position:relative}@media screen and (min-width: 60em){.md-search__form{border-radius:.1rem}}.md-search__input{position:relative;z-index:2;padding:0 2.2rem 0 3.6rem;text-overflow:ellipsis;background-color:var(--md-default-bg-color);transition:color 250ms,background-color 250ms}[dir=rtl] .md-search__input{padding:0 3.6rem 0 2.2rem}.md-search__input::placeholder{transition:color 250ms}.md-search__input~.md-search__icon,.md-search__input::placeholder{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width: 59.9375em){.md-search__input{width:100%;height:2.4rem;font-size:.9rem}}@media screen and (min-width: 60em){.md-search__input{width:100%;height:1.8rem;padding-left:2.2rem;color:inherit;font-size:.8rem;background-color:rgba(0,0,0,.26);border-radius:.1rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input:hover{background-color:rgba(255,255,255,.12)}[data-md-toggle=search]:checked~.md-header .md-search__input{color:var(--md-default-fg-color);text-overflow:clip;background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon,[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:var(--md-default-fg-color--light)}}.md-search__icon{position:absolute;z-index:2;width:1.2rem;height:1.2rem;cursor:pointer;transition:color 250ms,opacity 250ms}.md-search__icon:hover{opacity:.7}.md-search__icon[for=__search]{top:.3rem;left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem;left:initial}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width: 59.9375em){.md-search__icon[for=__search]{top:.6rem;left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem;left:initial}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width: 60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}.md-search__icon[type=reset]{top:.3rem;right:.5rem;transform:scale(0.75);opacity:0;transition:transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 150ms;pointer-events:none}[dir=rtl] .md-search__icon[type=reset]{right:initial;left:.5rem}@media screen and (max-width: 59.9375em){.md-search__icon[type=reset]{top:.6rem;right:.8rem}[dir=rtl] .md-search__icon[type=reset]{right:initial;left:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__input:not(:placeholder-shown)~.md-search__icon[type=reset]{transform:scale(1);opacity:1;pointer-events:initial}[data-md-toggle=search]:checked~.md-header .md-search__input:not(:placeholder-shown)~.md-search__icon[type=reset]:hover{opacity:.7}.md-search__output{position:absolute;z-index:1;width:100%;overflow:hidden;border-radius:0 0 .1rem .1rem}@media screen and (max-width: 59.9375em){.md-search__output{top:2.4rem;bottom:0}}@media screen and (min-width: 60em){.md-search__output{top:1.9rem;opacity:0;transition:opacity 400ms}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12),0 3px 5px -1px rgba(0,0,0,.4);opacity:1}}.md-search__scrollwrap{height:100%;overflow-y:auto;background-color:var(--md-default-bg-color);backface-visibility:hidden;scroll-snap-type:y mandatory;touch-action:pan-y}@media(max-resolution: 1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width: 60em)and (max-width: 76.1875em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width: 76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width: 60em){.md-search__scrollwrap{max-height:0;scrollbar-width:thin;scrollbar-color:var(--md-default-fg-color--lighter) transparent}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-search__scrollwrap::-webkit-scrollbar{width:.2rem;height:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{padding:0 .8rem;color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;background-color:var(--md-default-fg-color--lightest);scroll-snap-align:start}@media screen and (min-width: 60em){.md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem;padding-left:initial}}.md-search-result__list{margin:0;padding:0;list-style:none}.md-search-result__item{box-shadow:0 -0.05rem 0 var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;transition:background 250ms;scroll-snap-align:start}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:focus .md-search-result__article::before,.md-search-result__link:hover .md-search-result__article::before{opacity:.7}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more summary{display:block;padding:.75em .8rem;color:var(--md-typeset-a-color);font-size:.64rem;outline:0;cursor:pointer;transition:color 250ms,background-color 250ms;scroll-snap-align:start}.md-search-result__more summary:focus,.md-search-result__more summary:hover{color:var(--md-accent-fg-color);background-color:var(--md-accent-fg-color--transparent)}@media screen and (min-width: 60em){.md-search-result__more summary{padding-left:2.2rem}[dir=rtl] .md-search-result__more summary{padding-right:2.2rem;padding-left:.8rem}}.md-search-result__more summary::-webkit-details-marker{display:none}.md-search-result__more summary~*>*{opacity:.65}.md-search-result__article{position:relative;padding:0 .8rem;overflow:hidden}@media screen and (min-width: 60em){.md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem;padding-left:.8rem}}.md-search-result__article--document .md-search-result__title{margin:.55rem 0;font-weight:400;font-size:.8rem;line-height:1.4}.md-search-result__icon{position:absolute;left:0;width:1.2rem;height:1.2rem;margin:.5rem;color:var(--md-default-fg-color--light)}.md-search-result__icon::after{display:inline-block;width:100%;height:100%;background-color:currentColor;mask-image:var(--md-search-result-icon);mask-repeat:no-repeat;content:\"\"}[dir=rtl] .md-search-result__icon{right:0;left:initial}[dir=rtl] .md-search-result__icon::after{transform:scaleX(-1)}@media screen and (max-width: 59.9375em){.md-search-result__icon{display:none}}.md-search-result__title{margin:.5em 0;font-weight:700;font-size:.64rem;line-height:1.6}.md-search-result__teaser{display:-webkit-box;max-height:2rem;margin:.5em 0;overflow:hidden;color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6;text-overflow:ellipsis;-webkit-box-orient:vertical;-webkit-line-clamp:2}@media screen and (max-width: 44.9375em){.md-search-result__teaser{max-height:3rem;-webkit-line-clamp:3}}@media screen and (min-width: 60em)and (max-width: 76.1875em){.md-search-result__teaser{max-height:3rem;-webkit-line-clamp:3}}.md-search-result__teaser mark{background-color:transparent;border-bottom:.05rem solid var(--md-accent-fg-color)}.md-search-result__terms{margin:.5em 0;font-size:.64rem;font-style:italic}.md-search-result mark{color:var(--md-accent-fg-color);background-color:transparent}@keyframes md-sidebar__scrollwrap--hack{0%,99%{scroll-snap-type:none}100%{scroll-snap-type:y mandatory}}.md-sidebar{position:sticky;top:2.4rem;align-self:flex-start;width:12.1rem;padding:1.2rem 0;overflow:hidden}@media print{.md-sidebar{display:none}}@media screen and (max-width: 76.1875em){.md-sidebar--primary{position:fixed;top:0;left:-12.1rem;z-index:3;width:12.1rem;height:100%;background-color:var(--md-default-bg-color);transform:translateX(0);transition:transform 250ms cubic-bezier(0.4, 0, 0.2, 1),box-shadow 250ms}[dir=rtl] .md-sidebar--primary{right:-12.1rem;left:initial}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12),0 5px 5px -3px rgba(0,0,0,.4);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{overflow:hidden}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width: 60em){.md-sidebar--secondary{display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{max-height:100%;margin:0 .2rem;overflow-y:auto;backface-visibility:hidden;scrollbar-width:thin;scrollbar-color:var(--md-default-fg-color--lighter) transparent}.js .md-sidebar__scrollwrap{animation:md-sidebar__scrollwrap--hack 400ms forwards}@media screen and (max-width: 76.1875em){.md-sidebar--primary .md-sidebar__scrollwrap{position:absolute;top:0;right:0;bottom:0;left:0;margin:0;scroll-snap-type:none}}.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) transparent}.md-sidebar__scrollwrap::-webkit-scrollbar{width:.2rem;height:.2rem}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@keyframes md-source__facts--done{0%{height:0}100%{height:.65rem}}@keyframes md-source__fact--done{0%{transform:translateY(100%);opacity:0}50%{opacity:0}100%{transform:translateY(0%);opacity:1}}.md-source{display:block;font-size:.65rem;line-height:1.2;white-space:nowrap;backface-visibility:hidden;transition:opacity 250ms}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;width:2.4rem;height:2.4rem;vertical-align:middle}.md-source__icon svg{margin-top:.6rem;margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem;margin-left:initial}.md-source__icon+.md-source__repository{margin-left:-2rem;padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem;margin-left:initial;padding-right:2rem;padding-left:initial}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);margin-left:.6rem;overflow:hidden;font-weight:700;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{margin:0;padding:0;overflow:hidden;font-weight:700;font-size:.55rem;list-style-type:none;opacity:.75}[data-md-state=done] .md-source__facts{animation:md-source__facts--done 250ms ease-in}.md-source__fact{float:left}[dir=rtl] .md-source__fact{float:right}[data-md-state=done] .md-source__fact{animation:md-source__fact--done 400ms ease-out}.md-source__fact::before{margin:0 .1rem;content:\"·\"}.md-source__fact:first-child::before{display:none}.md-tabs{width:100%;overflow:auto;color:var(--md-primary-bg-color);background-color:var(--md-primary-fg-color);transition:background 250ms}.no-js .md-tabs{transition:none}@media screen and (max-width: 76.1875em){.md-tabs{display:none}}@media print{.md-tabs{display:none}}.md-tabs__list{margin:0;margin-left:.2rem;padding:0;white-space:nowrap;list-style:none;contain:content}[dir=rtl] .md-tabs__list{margin-right:.2rem;margin-left:initial}.md-tabs__item{display:inline-block;height:2.4rem;padding-right:.6rem;padding-left:.6rem}.md-tabs__link{display:block;margin-top:.8rem;font-size:.7rem;opacity:.7;transition:transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),opacity 250ms}.no-js .md-tabs__link{transition:none}.md-tabs__link--active,.md-tabs__link:hover{color:inherit;opacity:1}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:100ms}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:120ms}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:140ms}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:160ms}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:180ms}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:200ms}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:220ms}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:240ms}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:260ms}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:280ms}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:300ms}.md-tabs[data-md-state=hidden]{pointer-events:none}.md-tabs[data-md-state=hidden] .md-tabs__link{transform:translateY(50%);opacity:0;transition:color 250ms,transform 0ms 400ms,opacity 100ms}@media screen and (min-width: 76.25em){.md-tabs~.md-main .md-nav--primary>.md-nav__list>.md-nav__item--nested{display:none}.md-tabs--active~.md-main .md-nav--primary .md-nav__title{display:block;padding:0 .6rem;pointer-events:none;scroll-snap-align:start}.md-tabs--active~.md-main .md-nav--primary .md-nav__title[for=__drawer]{display:none}.md-tabs--active~.md-main .md-nav--primary>.md-nav__list>.md-nav__item{display:none}.md-tabs--active~.md-main .md-nav--primary>.md-nav__list>.md-nav__item--active{display:block;padding:0}.md-tabs--active~.md-main .md-nav--primary>.md-nav__list>.md-nav__item--active>.md-nav__link{display:none}.md-tabs--active~.md-main .md-nav[data-md-level=\"1\"]{display:block}.md-tabs--active~.md-main .md-nav[data-md-level=\"1\"]>.md-nav__list>.md-nav__item{padding:0 .6rem}.md-tabs--active~.md-main .md-nav[data-md-level=\"1\"]>.md-nav__list>.md-nav__item:last-child{padding-bottom:.6rem}.md-tabs--active~.md-main .md-nav[data-md-level=\"1\"]>.md-nav__list>.md-nav__item:last-child .md-nav__item{padding-bottom:0}.md-tabs--active~.md-main .md-nav[data-md-level=\"1\"] .md-nav .md-nav__title{display:none}}:root{--md-admonition-icon--note: svg-load(\"@mdi/svg/svg/pencil.svg\");--md-admonition-icon--abstract: svg-load(\"@mdi/svg/svg/text-subject.svg\");--md-admonition-icon--info: svg-load(\"@mdi/svg/svg/information.svg\");--md-admonition-icon--tip: svg-load(\"@mdi/svg/svg/fire.svg\");--md-admonition-icon--success: svg-load(\"@mdi/svg/svg/check-circle.svg\");--md-admonition-icon--question: svg-load(\"@mdi/svg/svg/help-circle.svg\");--md-admonition-icon--warning: svg-load(\"@mdi/svg/svg/alert.svg\");--md-admonition-icon--failure: svg-load(\"@mdi/svg/svg/close-circle.svg\");--md-admonition-icon--danger: svg-load(\"@mdi/svg/svg/flash-circle.svg\");--md-admonition-icon--bug: svg-load(\"@mdi/svg/svg/bug.svg\");--md-admonition-icon--example: svg-load(\"@mdi/svg/svg/format-list-numbered.svg\");--md-admonition-icon--quote: svg-load(\"@mdi/svg/svg/format-quote-close.svg\")}.md-typeset .admonition,.md-typeset details{margin:1.5625em 0;padding:0 .6rem;overflow:hidden;color:var(--md-admonition-fg-color);font-size:.64rem;page-break-inside:avoid;background-color:var(--md-admonition-bg-color);border-left:.2rem solid #448aff;border-radius:.1rem;box-shadow:0 .2rem .5rem rgba(0,0,0,.05),0 0 .05rem rgba(0,0,0,.1)}[dir=rtl] .md-typeset .admonition,[dir=rtl] .md-typeset details{border-right:.2rem solid #448aff;border-left:none}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}.md-typeset .admonition .admonition,.md-typeset details .admonition,.md-typeset .admonition details,.md-typeset details details{margin:1em 0}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -0.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}.md-typeset .admonition-title,.md-typeset summary{position:relative;margin:0 -0.6rem 0 -0.8rem;padding:.4rem .6rem .4rem 2.2rem;font-weight:700;background-color:rgba(68,138,255,.1)}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{margin:0 -0.8rem 0 -0.6rem;padding:.4rem 2rem .4rem .6rem}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}.md-typeset .admonition-title::before,.md-typeset summary::before{position:absolute;left:.8rem;width:1rem;height:1rem;background-color:#448aff;mask-image:var(--md-admonition-icon--note);mask-repeat:no-repeat;content:\"\"}[dir=rtl] .md-typeset .admonition-title::before,[dir=rtl] .md-typeset summary::before{right:.8rem;left:initial}.md-typeset .admonition-title code,.md-typeset summary code{margin:initial;padding:initial;color:currentColor;background-color:transparent;border-radius:initial;box-shadow:none}.md-typeset .admonition-title+.tabbed-set:last-child,.md-typeset summary+.tabbed-set:last-child{margin-top:0}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:rgba(68,138,255,.1)}.md-typeset .note>.admonition-title::before,.md-typeset .note>summary::before{background-color:#448aff;mask-image:var(--md-admonition-icon--note);mask-repeat:no-repeat}.md-typeset .admonition.abstract,.md-typeset details.abstract,.md-typeset .admonition.tldr,.md-typeset details.tldr,.md-typeset .admonition.summary,.md-typeset details.summary{border-color:#00b0ff}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary,.md-typeset .tldr>.admonition-title,.md-typeset .tldr>summary,.md-typeset .summary>.admonition-title,.md-typeset .summary>summary{background-color:rgba(0,176,255,.1)}.md-typeset .abstract>.admonition-title::before,.md-typeset .abstract>summary::before,.md-typeset .tldr>.admonition-title::before,.md-typeset .tldr>summary::before,.md-typeset .summary>.admonition-title::before,.md-typeset .summary>summary::before{background-color:#00b0ff;mask-image:var(--md-admonition-icon--abstract);mask-repeat:no-repeat}.md-typeset .admonition.info,.md-typeset details.info,.md-typeset .admonition.todo,.md-typeset details.todo{border-color:#00b8d4}.md-typeset .info>.admonition-title,.md-typeset .info>summary,.md-typeset .todo>.admonition-title,.md-typeset .todo>summary{background-color:rgba(0,184,212,.1)}.md-typeset .info>.admonition-title::before,.md-typeset .info>summary::before,.md-typeset .todo>.admonition-title::before,.md-typeset .todo>summary::before{background-color:#00b8d4;mask-image:var(--md-admonition-icon--info);mask-repeat:no-repeat}.md-typeset .admonition.tip,.md-typeset details.tip,.md-typeset .admonition.important,.md-typeset details.important,.md-typeset .admonition.hint,.md-typeset details.hint{border-color:#00bfa5}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary,.md-typeset .important>.admonition-title,.md-typeset .important>summary,.md-typeset .hint>.admonition-title,.md-typeset .hint>summary{background-color:rgba(0,191,165,.1)}.md-typeset .tip>.admonition-title::before,.md-typeset .tip>summary::before,.md-typeset .important>.admonition-title::before,.md-typeset .important>summary::before,.md-typeset .hint>.admonition-title::before,.md-typeset .hint>summary::before{background-color:#00bfa5;mask-image:var(--md-admonition-icon--tip);mask-repeat:no-repeat}.md-typeset .admonition.success,.md-typeset details.success,.md-typeset .admonition.done,.md-typeset details.done,.md-typeset .admonition.check,.md-typeset details.check{border-color:#00c853}.md-typeset .success>.admonition-title,.md-typeset .success>summary,.md-typeset .done>.admonition-title,.md-typeset .done>summary,.md-typeset .check>.admonition-title,.md-typeset .check>summary{background-color:rgba(0,200,83,.1)}.md-typeset .success>.admonition-title::before,.md-typeset .success>summary::before,.md-typeset .done>.admonition-title::before,.md-typeset .done>summary::before,.md-typeset .check>.admonition-title::before,.md-typeset .check>summary::before{background-color:#00c853;mask-image:var(--md-admonition-icon--success);mask-repeat:no-repeat}.md-typeset .admonition.question,.md-typeset details.question,.md-typeset .admonition.faq,.md-typeset details.faq,.md-typeset .admonition.help,.md-typeset details.help{border-color:#64dd17}.md-typeset .question>.admonition-title,.md-typeset .question>summary,.md-typeset .faq>.admonition-title,.md-typeset .faq>summary,.md-typeset .help>.admonition-title,.md-typeset .help>summary{background-color:rgba(100,221,23,.1)}.md-typeset .question>.admonition-title::before,.md-typeset .question>summary::before,.md-typeset .faq>.admonition-title::before,.md-typeset .faq>summary::before,.md-typeset .help>.admonition-title::before,.md-typeset .help>summary::before{background-color:#64dd17;mask-image:var(--md-admonition-icon--question);mask-repeat:no-repeat}.md-typeset .admonition.warning,.md-typeset details.warning,.md-typeset .admonition.attention,.md-typeset details.attention,.md-typeset .admonition.caution,.md-typeset details.caution{border-color:#ff9100}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary,.md-typeset .attention>.admonition-title,.md-typeset .attention>summary,.md-typeset .caution>.admonition-title,.md-typeset .caution>summary{background-color:rgba(255,145,0,.1)}.md-typeset .warning>.admonition-title::before,.md-typeset .warning>summary::before,.md-typeset .attention>.admonition-title::before,.md-typeset .attention>summary::before,.md-typeset .caution>.admonition-title::before,.md-typeset .caution>summary::before{background-color:#ff9100;mask-image:var(--md-admonition-icon--warning);mask-repeat:no-repeat}.md-typeset .admonition.failure,.md-typeset details.failure,.md-typeset .admonition.missing,.md-typeset details.missing,.md-typeset .admonition.fail,.md-typeset details.fail{border-color:#ff5252}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary,.md-typeset .missing>.admonition-title,.md-typeset .missing>summary,.md-typeset .fail>.admonition-title,.md-typeset .fail>summary{background-color:rgba(255,82,82,.1)}.md-typeset .failure>.admonition-title::before,.md-typeset .failure>summary::before,.md-typeset .missing>.admonition-title::before,.md-typeset .missing>summary::before,.md-typeset .fail>.admonition-title::before,.md-typeset .fail>summary::before{background-color:#ff5252;mask-image:var(--md-admonition-icon--failure);mask-repeat:no-repeat}.md-typeset .admonition.danger,.md-typeset details.danger,.md-typeset .admonition.error,.md-typeset details.error{border-color:#ff1744}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary,.md-typeset .error>.admonition-title,.md-typeset .error>summary{background-color:rgba(255,23,68,.1)}.md-typeset .danger>.admonition-title::before,.md-typeset .danger>summary::before,.md-typeset .error>.admonition-title::before,.md-typeset .error>summary::before{background-color:#ff1744;mask-image:var(--md-admonition-icon--danger);mask-repeat:no-repeat}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:rgba(245,0,87,.1)}.md-typeset .bug>.admonition-title::before,.md-typeset .bug>summary::before{background-color:#f50057;mask-image:var(--md-admonition-icon--bug);mask-repeat:no-repeat}.md-typeset .admonition.example,.md-typeset details.example{border-color:#651fff}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:rgba(101,31,255,.1)}.md-typeset .example>.admonition-title::before,.md-typeset .example>summary::before{background-color:#651fff;mask-image:var(--md-admonition-icon--example);mask-repeat:no-repeat}.md-typeset .admonition.quote,.md-typeset details.quote,.md-typeset .admonition.cite,.md-typeset details.cite{border-color:#9e9e9e}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary,.md-typeset .cite>.admonition-title,.md-typeset .cite>summary{background-color:rgba(158,158,158,.1)}.md-typeset .quote>.admonition-title::before,.md-typeset .quote>summary::before,.md-typeset .cite>.admonition-title::before,.md-typeset .cite>summary::before{background-color:#9e9e9e;mask-image:var(--md-admonition-icon--quote);mask-repeat:no-repeat}.codehilite .o,.highlight .o,.codehilite .ow,.highlight .ow{color:var(--md-code-hl-operator-color)}.codehilite .p,.highlight .p{color:var(--md-code-hl-punctuation-color)}.codehilite .cpf,.highlight .cpf,.codehilite .l,.highlight .l,.codehilite .s,.highlight .s,.codehilite .sb,.highlight .sb,.codehilite .sc,.highlight .sc,.codehilite .s2,.highlight .s2,.codehilite .si,.highlight .si,.codehilite .s1,.highlight .s1,.codehilite .ss,.highlight .ss{color:var(--md-code-hl-string-color)}.codehilite .cp,.highlight .cp,.codehilite .se,.highlight .se,.codehilite .sh,.highlight .sh,.codehilite .sr,.highlight .sr,.codehilite .sx,.highlight .sx{color:var(--md-code-hl-special-color)}.codehilite .m,.highlight .m,.codehilite .mf,.highlight .mf,.codehilite .mh,.highlight .mh,.codehilite .mi,.highlight .mi,.codehilite .il,.highlight .il,.codehilite .mo,.highlight .mo{color:var(--md-code-hl-number-color)}.codehilite .k,.highlight .k,.codehilite .kd,.highlight .kd,.codehilite .kn,.highlight .kn,.codehilite .kp,.highlight .kp,.codehilite .kr,.highlight .kr,.codehilite .kt,.highlight .kt{color:var(--md-code-hl-keyword-color)}.codehilite .kc,.highlight .kc,.codehilite .n,.highlight .n{color:var(--md-code-hl-name-color)}.codehilite .no,.highlight .no,.codehilite .nb,.highlight .nb,.codehilite .bp,.highlight .bp{color:var(--md-code-hl-constant-color)}.codehilite .nc,.highlight .nc,.codehilite .ne,.highlight .ne,.codehilite .nf,.highlight .nf,.codehilite .nn,.highlight .nn{color:var(--md-code-hl-function-color)}.codehilite .nd,.highlight .nd,.codehilite .ni,.highlight .ni,.codehilite .nl,.highlight .nl,.codehilite .nt,.highlight .nt{color:var(--md-code-hl-keyword-color)}.codehilite .c,.highlight .c,.codehilite .cm,.highlight .cm,.codehilite .c1,.highlight .c1,.codehilite .ch,.highlight .ch,.codehilite .cs,.highlight .cs,.codehilite .sd,.highlight .sd{color:var(--md-code-hl-comment-color)}.codehilite .na,.highlight .na,.codehilite .nv,.highlight .nv,.codehilite .vc,.highlight .vc,.codehilite .vg,.highlight .vg,.codehilite .vi,.highlight .vi{color:var(--md-code-hl-variable-color)}.codehilite .ge,.highlight .ge,.codehilite .gr,.highlight .gr,.codehilite .gh,.highlight .gh,.codehilite .go,.highlight .go,.codehilite .gp,.highlight .gp,.codehilite .gs,.highlight .gs,.codehilite .gu,.highlight .gu,.codehilite .gt,.highlight .gt{color:var(--md-code-hl-generic-color)}.codehilite .gd,.highlight .gd,.codehilite .gi,.highlight .gi{margin:0 -0.125em;padding:0 .125em;border-radius:.1rem}.codehilite .gd,.highlight .gd{background-color:var(--md-typeset-del-color)}.codehilite .gi,.highlight .gi{background-color:var(--md-typeset-ins-color)}.codehilite .hll,.highlight .hll{display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em;background-color:var(--md-code-hl-color)}.codehilitetable,.highlighttable{display:block;overflow:hidden}.codehilitetable tbody,.highlighttable tbody,.codehilitetable td,.highlighttable td{display:block;padding:0}.codehilitetable tr,.highlighttable tr{display:flex}.codehilitetable pre,.highlighttable pre{margin:0}.codehilitetable .linenos,.highlighttable .linenos{padding:.525rem 1.1764705882em;padding-right:0;font-size:.85em;background-color:var(--md-code-bg-color);user-select:none}.codehilitetable .linenodiv,.highlighttable .linenodiv{padding-right:.5882352941em;box-shadow:-0.05rem 0 var(--md-default-fg-color--lighter) inset}.codehilitetable .linenodiv pre,.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.codehilitetable .code,.highlighttable .code{flex:1;overflow:hidden}.md-typeset .codehilitetable,.md-typeset .highlighttable{margin:1em 0;direction:ltr;border-radius:.1rem}.md-typeset .codehilitetable code,.md-typeset .highlighttable code{border-radius:0}@media screen and (max-width: 44.9375em){.md-typeset>.codehilite,.md-typeset>.highlight{margin:1em -0.8rem}.md-typeset>.codehilite .hll,.md-typeset>.highlight .hll{margin:0 -0.8rem;padding:0 .8rem}.md-typeset>.codehilite code,.md-typeset>.highlight code{border-radius:0}.md-typeset>.codehilitetable,.md-typeset>.highlighttable{margin:1em -0.8rem;border-radius:0}.md-typeset>.codehilitetable .hll,.md-typeset>.highlighttable .hll{margin:0 -0.8rem;padding:0 .8rem}}:root{--md-footnotes-icon: svg-load(\"@mdi/svg/svg/keyboard-return.svg\")}.md-typeset [id^=\"fnref:\"]{display:inline-block}.md-typeset [id^=\"fnref:\"]:target{margin-top:-3.8rem;padding-top:3.8rem;pointer-events:none;scroll-margin-top:initial}.md-typeset [id^=\"fn:\"]::before{display:none;height:0;content:\"\"}.md-typeset [id^=\"fn:\"]:target{scroll-margin-top:initial}.md-typeset [id^=\"fn:\"]:target::before{display:block;margin-top:-3.5rem;padding-top:3.5rem;pointer-events:none}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}.md-typeset .footnote ol{margin-left:0}.md-typeset .footnote li{transition:color 125ms}.md-typeset .footnote li:target{color:var(--md-default-fg-color)}.md-typeset .footnote li :first-child{margin-top:0}.md-typeset .footnote li:hover .footnote-backref,.md-typeset .footnote li:target .footnote-backref{transform:translateX(0);opacity:1}.md-typeset .footnote li:hover .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-ref{display:inline-block;pointer-events:initial}.md-typeset .footnote-backref{display:inline-block;color:var(--md-typeset-a-color);font-size:0;vertical-align:text-bottom;transform:translateX(0.25rem);opacity:0;transition:color 250ms,transform 250ms 250ms,opacity 125ms 250ms}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-0.25rem)}.md-typeset .footnote-backref::before{display:inline-block;width:.8rem;height:.8rem;background-color:currentColor;mask-image:var(--md-footnotes-icon);mask-repeat:no-repeat;content:\"\"}[dir=rtl] .md-typeset .footnote-backref::before svg{transform:scaleX(-1)}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);transform:translateX(0);opacity:1}}.md-typeset .headerlink{display:inline-block;margin-left:.5rem;visibility:hidden;opacity:0;transition:color 250ms,visibility 0ms 500ms,opacity 125ms}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem;margin-left:initial}html body .md-typeset .headerlink{color:var(--md-default-fg-color--lighter)}@media print{.md-typeset .headerlink{display:none}}.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink,.md-typeset .headerlink:focus{visibility:visible;opacity:1;transition:color 250ms,visibility 0ms,opacity 125ms}.md-typeset :target>.headerlink,.md-typeset .headerlink:focus,.md-typeset .headerlink:hover{color:var(--md-accent-fg-color)}.md-typeset :target{scroll-margin-top:3.6rem}.md-typeset h3[id]:target,.md-typeset h2[id]:target,.md-typeset h1[id]:target{scroll-margin-top:initial}.md-typeset h3[id]::before,.md-typeset h2[id]::before,.md-typeset h1[id]::before{display:block;margin-top:-0.4rem;padding-top:.4rem;content:\"\"}.md-typeset h3[id]:target::before,.md-typeset h2[id]:target::before,.md-typeset h1[id]:target::before{margin-top:-3.4rem;padding-top:3.4rem}.md-typeset h4[id]:target{scroll-margin-top:initial}.md-typeset h4[id]::before{display:block;margin-top:-0.45rem;padding-top:.45rem;content:\"\"}.md-typeset h4[id]:target::before{margin-top:-3.45rem;padding-top:3.45rem}.md-typeset h6[id]:target,.md-typeset h5[id]:target{scroll-margin-top:initial}.md-typeset h6[id]::before,.md-typeset h5[id]::before{display:block;margin-top:-0.6rem;padding-top:.6rem;content:\"\"}.md-typeset h6[id]:target::before,.md-typeset h5[id]:target::before{margin-top:-3.6rem;padding-top:3.6rem}.md-typeset div.arithmatex{overflow-x:scroll}@media screen and (max-width: 44.9375em){.md-typeset div.arithmatex{margin:0 -0.8rem}}.md-typeset div.arithmatex>*{width:min-content;margin:1em auto !important;padding:0 .8rem;overflow:auto;touch-action:auto}.md-typeset del.critic,.md-typeset ins.critic,.md-typeset .critic.comment{box-decoration-break:clone}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment::before{content:\"/* \"}.md-typeset .critic.comment::after{content:\" */\"}.md-typeset .critic.block{display:block;margin:1em 0;padding-right:.8rem;padding-left:.8rem;overflow:auto;box-shadow:none}.md-typeset .critic.block :first-child{margin-top:.5em}.md-typeset .critic.block :last-child{margin-bottom:.5em}:root{--md-details-icon: svg-load(\"@mdi/svg/svg/chevron-right.svg\")}.md-typeset details{display:block;padding-top:0;overflow:visible}.md-typeset details[open]>summary::after{transform:rotate(90deg)}.md-typeset details:not([open]){padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}.md-typeset details::after{display:table;content:\"\"}.md-typeset summary{display:block;min-height:1rem;padding:.4rem 1.8rem .4rem 2.2rem;border-top-left-radius:.1rem;border-top-right-radius:.1rem;cursor:pointer}.md-typeset summary:not(.focus-visible){outline:none;-webkit-tap-highlight-color:transparent}[dir=rtl] .md-typeset summary{padding:.4rem 2.2rem .4rem 1.8rem}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset summary::after{position:absolute;top:.4rem;right:.4rem;width:1rem;height:1rem;background-color:currentColor;mask-image:var(--md-details-icon);mask-repeat:no-repeat;transform:rotate(0deg);transition:transform 250ms;content:\"\"}[dir=rtl] .md-typeset summary::after{right:initial;left:.4rem;transform:rotate(180deg)}.md-typeset img.emojione,.md-typeset img.twemoji,.md-typeset img.gemoji{width:1.125em;max-height:100%;vertical-align:-15%}.md-typeset span.twemoji{display:inline-block;height:1.125em;vertical-align:text-top}.md-typeset span.twemoji svg{width:1.125em;max-height:100%;fill:currentColor}.highlight [data-linenos]::before{position:sticky;left:-1.1764705882em;float:left;margin-right:1.1764705882em;margin-left:-1.1764705882em;padding-left:1.1764705882em;color:var(--md-default-fg-color--light);background-color:var(--md-code-bg-color);box-shadow:-0.05rem 0 var(--md-default-fg-color--lighter) inset;content:attr(data-linenos);user-select:none}.md-typeset .keys kbd::before,.md-typeset .keys kbd::after{position:relative;margin:0;color:inherit;-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial}.md-typeset .keys span{padding:0 .2em;color:var(--md-default-fg-color--light)}.md-typeset .keys .key-alt::before{padding-right:.4em;content:\"⎇\"}.md-typeset .keys .key-left-alt::before{padding-right:.4em;content:\"⎇\"}.md-typeset .keys .key-right-alt::before{padding-right:.4em;content:\"⎇\"}.md-typeset .keys .key-command::before{padding-right:.4em;content:\"⌘\"}.md-typeset .keys .key-left-command::before{padding-right:.4em;content:\"⌘\"}.md-typeset .keys .key-right-command::before{padding-right:.4em;content:\"⌘\"}.md-typeset .keys .key-control::before{padding-right:.4em;content:\"⌃\"}.md-typeset .keys .key-left-control::before{padding-right:.4em;content:\"⌃\"}.md-typeset .keys .key-right-control::before{padding-right:.4em;content:\"⌃\"}.md-typeset .keys .key-meta::before{padding-right:.4em;content:\"◆\"}.md-typeset .keys .key-left-meta::before{padding-right:.4em;content:\"◆\"}.md-typeset .keys .key-right-meta::before{padding-right:.4em;content:\"◆\"}.md-typeset .keys .key-option::before{padding-right:.4em;content:\"⌥\"}.md-typeset .keys .key-left-option::before{padding-right:.4em;content:\"⌥\"}.md-typeset .keys .key-right-option::before{padding-right:.4em;content:\"⌥\"}.md-typeset .keys .key-shift::before{padding-right:.4em;content:\"⇧\"}.md-typeset .keys .key-left-shift::before{padding-right:.4em;content:\"⇧\"}.md-typeset .keys .key-right-shift::before{padding-right:.4em;content:\"⇧\"}.md-typeset .keys .key-super::before{padding-right:.4em;content:\"❖\"}.md-typeset .keys .key-left-super::before{padding-right:.4em;content:\"❖\"}.md-typeset .keys .key-right-super::before{padding-right:.4em;content:\"❖\"}.md-typeset .keys .key-windows::before{padding-right:.4em;content:\"⊞\"}.md-typeset .keys .key-left-windows::before{padding-right:.4em;content:\"⊞\"}.md-typeset .keys .key-right-windows::before{padding-right:.4em;content:\"⊞\"}.md-typeset .keys .key-arrow-down::before{padding-right:.4em;content:\"↓\"}.md-typeset .keys .key-arrow-left::before{padding-right:.4em;content:\"←\"}.md-typeset .keys .key-arrow-right::before{padding-right:.4em;content:\"→\"}.md-typeset .keys .key-arrow-up::before{padding-right:.4em;content:\"↑\"}.md-typeset .keys .key-backspace::before{padding-right:.4em;content:\"⌫\"}.md-typeset .keys .key-backtab::before{padding-right:.4em;content:\"⇤\"}.md-typeset .keys .key-caps-lock::before{padding-right:.4em;content:\"⇪\"}.md-typeset .keys .key-clear::before{padding-right:.4em;content:\"⌧\"}.md-typeset .keys .key-context-menu::before{padding-right:.4em;content:\"☰\"}.md-typeset .keys .key-delete::before{padding-right:.4em;content:\"⌦\"}.md-typeset .keys .key-eject::before{padding-right:.4em;content:\"⏏\"}.md-typeset .keys .key-end::before{padding-right:.4em;content:\"⤓\"}.md-typeset .keys .key-escape::before{padding-right:.4em;content:\"⎋\"}.md-typeset .keys .key-home::before{padding-right:.4em;content:\"⤒\"}.md-typeset .keys .key-insert::before{padding-right:.4em;content:\"⎀\"}.md-typeset .keys .key-page-down::before{padding-right:.4em;content:\"⇟\"}.md-typeset .keys .key-page-up::before{padding-right:.4em;content:\"⇞\"}.md-typeset .keys .key-print-screen::before{padding-right:.4em;content:\"⎙\"}.md-typeset .keys .key-tab::after{padding-left:.4em;content:\"⇥\"}.md-typeset .keys .key-num-enter::after{padding-left:.4em;content:\"⌤\"}.md-typeset .keys .key-enter::after{padding-left:.4em;content:\"⏎\"}.md-typeset .tabbed-content{display:none;order:99;width:100%;box-shadow:0 -0.05rem var(--md-default-fg-color--lightest)}.md-typeset .tabbed-content>pre:only-child,.md-typeset .tabbed-content>.codehilite:only-child pre,.md-typeset .tabbed-content>.codehilitetable:only-child,.md-typeset .tabbed-content>.highlight:only-child pre,.md-typeset .tabbed-content>.highlighttable:only-child{margin:0}.md-typeset .tabbed-content>pre:only-child>code,.md-typeset .tabbed-content>.codehilite:only-child pre>code,.md-typeset .tabbed-content>.codehilitetable:only-child>code,.md-typeset .tabbed-content>.highlight:only-child pre>code,.md-typeset .tabbed-content>.highlighttable:only-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-content>.tabbed-set{margin:0}.md-typeset .tabbed-set{position:relative;display:flex;flex-wrap:wrap;margin:1em 0;border-radius:.1rem}.md-typeset .tabbed-set>input{position:absolute;width:0;height:0;opacity:0}.md-typeset .tabbed-set>input:checked+label{color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:checked+label+.tabbed-content{display:block}.md-typeset .tabbed-set>input:focus+label{outline-style:auto}.md-typeset .tabbed-set>input:not(.focus-visible)+label{outline:none;-webkit-tap-highlight-color:transparent}.md-typeset .tabbed-set>label{z-index:1;width:auto;padding:.9375em 1.25em .78125em;color:var(--md-default-fg-color--light);font-weight:700;font-size:.64rem;border-bottom:.1rem solid transparent;cursor:pointer;transition:color 250ms}html .md-typeset .tabbed-set>label:hover{color:var(--md-accent-fg-color)}:root{--md-tasklist-icon: svg-load( \"@primer/octicons/build/svg/check-circle-fill-24.svg\" );--md-tasklist-icon--checked: svg-load( \"@primer/octicons/build/svg/check-circle-fill-24.svg\" )}.md-typeset .task-list-item{position:relative;list-style-type:none}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em;left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em;left:initial}.md-typeset .task-list-control .task-list-indicator::before{position:absolute;top:.15em;left:-1.5em;width:1.25em;height:1.25em;background-color:var(--md-default-fg-color--lightest);mask-image:var(--md-tasklist-icon);mask-repeat:no-repeat;content:\"\"}[dir=rtl] .md-typeset .task-list-control .task-list-indicator::before{right:-1.5em;left:initial}.md-typeset .task-list-control [type=checkbox]:checked+.task-list-indicator::before{background-color:#00e676;mask-image:var(--md-tasklist-icon--checked)}.md-typeset .task-list-control [type=checkbox]{z-index:-1;opacity:0}","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// stylelint-disable no-duplicate-selectors\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Enforce correct box model and prevent adjustments of font size after orientation changes in IE and iOS\nhtml {\n box-sizing: border-box;\n text-size-adjust: none;\n}\n\n// All elements shall inherit the document default\n*,\n*::before,\n*::after {\n box-sizing: inherit;\n}\n\n// Remove margin in all browsers\nbody {\n margin: 0;\n}\n\n// Reset horizontal rules in FF\nhr {\n box-sizing: content-box;\n overflow: visible;\n}\n\n// Reset tap outlines on iOS and Android\na,\nbutton,\nlabel,\ninput {\n -webkit-tap-highlight-color: transparent;\n}\n\n// Reset link styles\na {\n color: inherit;\n text-decoration: none;\n}\n\n// Normalize font-size in all browsers\nsmall {\n font-size: 80%;\n}\n\n// Prevent subscript and superscript from affecting line-height\nsub,\nsup {\n line-height: 1em;\n}\n\n// Remove borders on images\nimg {\n border-style: none;\n}\n\n// Reset table styles\ntable {\n border-collapse: separate;\n border-spacing: 0;\n}\n\n// Reset table cell styles\ntd,\nth {\n font-weight: normal; // stylelint-disable-line\n vertical-align: top;\n}\n\n// Reset button styles\nbutton {\n margin: 0;\n padding: 0;\n font-size: inherit;\n background: transparent;\n border: 0;\n}\n\n// Reset input styles\ninput {\n border: 0;\n outline: none;\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Color definitions\n:root {\n\n // Default color shades\n --md-default-fg-color: hsla(0, 0%, 0%, 0.87);\n --md-default-fg-color--light: hsla(0, 0%, 0%, 0.54);\n --md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32);\n --md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07);\n --md-default-bg-color: hsla(0, 0%, 100%, 1);\n --md-default-bg-color--light: hsla(0, 0%, 100%, 0.7);\n --md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3);\n --md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12);\n\n // Primary color shades\n --md-primary-fg-color: hsla(#{hex2hsl($clr-indigo-500)}, 1);\n --md-primary-fg-color--light: hsla(#{hex2hsl($clr-indigo-300)}, 1);\n --md-primary-fg-color--dark: hsla(#{hex2hsl($clr-indigo-700)}, 1);\n --md-primary-bg-color: hsla(0, 0%, 100%, 1);\n --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);\n\n // Accent color shades\n --md-accent-fg-color: hsla(#{hex2hsl($clr-indigo-a200)}, 1);\n --md-accent-fg-color--transparent: hsla(#{hex2hsl($clr-indigo-a200)}, 0.1);\n --md-accent-bg-color: hsla(0, 0%, 100%, 1);\n --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7);\n\n // Light theme (default)\n > * {\n\n // Code color shades\n --md-code-fg-color: hsla(200, 18%, 26%, 1);\n --md-code-bg-color: hsla(0, 0%, 96%, 1);\n\n // Code highlighting color shades\n --md-code-hl-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5);\n --md-code-hl-number-color: hsla(0, 67%, 50%, 1);\n --md-code-hl-special-color: hsla(340, 83%, 47%, 1);\n --md-code-hl-function-color: hsla(291, 45%, 50%, 1);\n --md-code-hl-constant-color: hsla(250, 63%, 60%, 1);\n --md-code-hl-keyword-color: hsla(219, 54%, 51%, 1);\n --md-code-hl-string-color: hsla(150, 63%, 30%, 1);\n --md-code-hl-name-color: var(--md-code-fg-color);\n --md-code-hl-operator-color: var(--md-default-fg-color--light);\n --md-code-hl-punctuation-color: var(--md-default-fg-color--light);\n --md-code-hl-comment-color: var(--md-default-fg-color--light);\n --md-code-hl-generic-color: var(--md-default-fg-color--light);\n --md-code-hl-variable-color: var(--md-default-fg-color--light);\n\n // Typeset color shades\n --md-typeset-color: var(--md-default-fg-color);\n --md-typeset-a-color: var(--md-primary-fg-color);\n\n // Typeset `mark` color shades\n --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5);\n\n // Typeset `del` and `ins` color shades\n --md-typeset-del-color: hsla(6, 90%, 60%, 0.15);\n --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15);\n\n // Typeset `kbd` color shades\n --md-typeset-kbd-color: hsla(0, 0%, 98%, 1);\n --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1);\n --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1);\n\n // Admonition color shades\n --md-admonition-fg-color: var(--md-default-fg-color);\n --md-admonition-bg-color: var(--md-default-bg-color);\n\n // Footer color shades\n --md-footer-fg-color: hsla(0, 0%, 100%, 1);\n --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7);\n --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.3);\n --md-footer-bg-color: hsla(0, 0%, 0%, 0.87);\n --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32);\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon\n.md-icon {\n\n // SVG defaults\n svg {\n display: block;\n width: px2rem(24px);\n height: px2rem(24px);\n margin: 0 auto;\n fill: currentColor;\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules: font definitions\n// ----------------------------------------------------------------------------\n\n// Enable font-smoothing in Webkit and FF\nbody {\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Default fonts\nbody,\ninput {\n color: var(--md-typeset-color);\n font-feature-settings: \"kern\", \"liga\";\n font-family: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;\n}\n\n// Proportionally spaced fonts\ncode,\npre,\nkbd {\n color: var(--md-typeset-color);\n font-feature-settings: \"kern\";\n font-family: SFMono-Regular, Consolas, Menlo, monospace;\n}\n\n// ----------------------------------------------------------------------------\n// Rules: typesetted content\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-typeset-table--ascending: svg-load(\"@mdi/svg/svg/arrow-down.svg\");\n --md-typeset-table--descending: svg-load(\"@mdi/svg/svg/arrow-up.svg\");\n}\n\n// ----------------------------------------------------------------------------\n\n// Content that is typeset - if possible, all margins, paddings and font sizes\n// should be set in ems, so nested blocks (e.g. admonitions) render correctly,\n// except headlines that should only appear on the top level and need to have\n// consistent spacing due to layout constraints.\n.md-typeset {\n font-size: px2rem(16px);\n line-height: 1.6;\n color-adjust: exact;\n\n // We'll use a smaller font-size for printing, so code examples don't break\n // too early, and 16px looks too big anyway.\n @media print {\n font-size: px2rem(13.6px);\n }\n\n // Default spacing\n p,\n ul,\n ol,\n blockquote {\n margin: 1em 0;\n }\n\n // 1st level headline\n h1 {\n margin: 0 0 px2em(40px, 32px);\n color: var(--md-default-fg-color--light);\n font-weight: 300;\n font-size: px2em(32px);\n line-height: 1.3;\n letter-spacing: -0.01em;\n }\n\n // 2nd level headline\n h2 {\n margin: px2em(40px, 25px) 0 px2em(16px, 25px);\n font-weight: 300;\n font-size: px2em(25px);\n line-height: 1.4;\n letter-spacing: -0.01em;\n }\n\n // 3rd level headline\n h3 {\n margin: px2em(32px, 20px) 0 px2em(16px, 20px);\n font-weight: 400;\n font-size: px2em(20px);\n line-height: 1.5;\n letter-spacing: -0.01em;\n }\n\n // 3rd level headline following an 2nd level headline\n h2 + h3 {\n margin-top: px2em(16px, 20px);\n }\n\n // 4th level headline\n h4 {\n margin: px2em(16px) 0;\n font-weight: 700;\n letter-spacing: -0.01em;\n }\n\n // 5th and 6th level headline\n h5,\n h6 {\n margin: px2em(16px, 12.8px) 0;\n color: var(--md-default-fg-color--light);\n font-weight: 700;\n font-size: px2em(12.8px);\n letter-spacing: -0.01em;\n }\n\n // Overrides for 5th level headline\n h5 {\n text-transform: uppercase;\n }\n\n // Horizontal separators\n hr {\n margin: 1.5em 0;\n border-bottom: px2rem(1px) dotted var(--md-default-fg-color--lighter);\n }\n\n // Links\n a {\n color: var(--md-typeset-a-color);\n word-break: break-word;\n\n // Also enable color transition on pseudo elements\n &,\n &::before {\n transition: color 125ms;\n }\n\n // Focused or hovered links\n &:focus,\n &:hover {\n color: var(--md-accent-fg-color);\n }\n }\n\n // Code blocks\n code,\n pre,\n kbd {\n color: var(--md-code-fg-color);\n direction: ltr;\n\n // Wrap text and hide scollbars\n @media print {\n white-space: pre-wrap;\n }\n }\n\n // Inline code blocks\n code {\n padding: 0 px2em(4px, 13.6px);\n font-size: px2em(13.6px);\n word-break: break-word;\n background-color: var(--md-code-bg-color);\n border-radius: px2rem(2px);\n box-decoration-break: clone;\n }\n\n // Disable containing block inside headlines\n h1 code,\n h2 code,\n h3 code,\n h4 code,\n h5 code,\n h6 code {\n margin: initial;\n padding: initial;\n background-color: transparent;\n box-shadow: none;\n }\n\n // Ensure link color in code blocks\n a > code {\n color: currentColor;\n }\n\n // Unformatted code blocks\n pre {\n position: relative;\n margin: 1em 0;\n line-height: 1.4;\n\n // Actual container with code, overflowing\n > code {\n display: block;\n margin: 0;\n padding: px2em(10.5px, 13.6px) px2em(16px, 13.6px);\n overflow: auto;\n word-break: normal;\n box-shadow: none;\n box-decoration-break: slice;\n touch-action: auto;\n // Override Firefox scrollbar style\n scrollbar-width: thin;\n scrollbar-color: var(--md-default-fg-color--lighter) transparent;\n\n // Override Firefox scrollbar hover color\n &:hover {\n scrollbar-color: var(--md-accent-fg-color) transparent;\n }\n\n // Override native scrollbar styles\n &::-webkit-scrollbar {\n width: px2rem(4px);\n height: px2rem(4px);\n }\n\n // Scrollbar thumb\n &::-webkit-scrollbar-thumb {\n background-color: var(--md-default-fg-color--lighter);\n\n // Hovered scrollbar thumb\n &:hover {\n background-color: var(--md-accent-fg-color);\n }\n }\n }\n }\n\n // [mobile -]: Stretch to whole width\n @include break-to-device(mobile) {\n\n // Stretch top-level containers\n > pre {\n margin: 1em px2rem(-16px);\n\n // Remove rounded borders\n code {\n border-radius: 0;\n }\n }\n }\n\n // Keyboard key\n kbd {\n display: inline-block;\n padding: 0 px2em(8px, 12px);\n color: var(--md-default-fg-color);\n font-size: px2em(12px);\n vertical-align: text-top;\n word-break: break-word;\n background-color: var(--md-typeset-kbd-color);\n border-radius: px2rem(2px);\n box-shadow:\n 0 px2rem(2px) 0 px2rem(1px) var(--md-typeset-kbd-border-color),\n 0 px2rem(2px) 0 var(--md-typeset-kbd-border-color),\n 0 px2rem(-2px) px2rem(4px) var(--md-typeset-kbd-accent-color) inset;\n }\n\n // Text highlighting marker\n mark {\n color: inherit;\n word-break: break-word;\n background-color: var(--md-typeset-mark-color);\n box-decoration-break: clone;\n }\n\n // Abbreviations\n abbr {\n text-decoration: none;\n border-bottom: px2rem(1px) dotted var(--md-default-fg-color--light);\n cursor: help;\n\n // Render a tooltip for touch devices\n @media (hover: none) {\n position: relative;\n\n // Tooltip\n &[title]:focus::after,\n &[title]:hover::after {\n @include z-depth(2);\n\n position: absolute;\n left: 0;\n display: inline-block;\n width: auto;\n min-width: max-content;\n max-width: 80%;\n margin-top: 2em;\n padding: px2rem(4px) px2rem(6px);\n color: var(--md-default-bg-color);\n font-size: px2rem(14px);\n background: var(--md-default-fg-color);\n border-radius: px2rem(2px);\n content: attr(title);\n }\n }\n\n }\n\n // Small text\n small {\n opacity: 0.75;\n }\n\n // Superscript and subscript\n sup,\n sub {\n margin-left: px2em(1px, 12.8px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2em(1px, 12.8px);\n margin-left: initial;\n }\n }\n\n // Blockquotes, possibly nested\n blockquote {\n padding-left: px2rem(12px);\n color: var(--md-default-fg-color--light);\n border-left: px2rem(4px) solid var(--md-default-fg-color--lighter);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(12px);\n padding-left: initial;\n border-right: px2rem(4px) solid var(--md-default-fg-color--lighter);\n border-left: initial;\n }\n }\n\n // Unordered lists\n ul {\n list-style-type: disc;\n }\n\n // Unordered and ordered lists\n ul,\n ol {\n margin-left: px2em(10px);\n padding: 0;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2em(10px);\n margin-left: initial;\n }\n\n // Nested ordered lists\n ol {\n list-style-type: lower-alpha;\n\n // Triply nested ordered list\n ol {\n list-style-type: lower-roman;\n }\n }\n\n // List elements\n li {\n margin-bottom: 0.5em;\n margin-left: px2em(20px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2em(20px);\n margin-left: initial;\n }\n\n // Decrease vertical spacing\n p,\n blockquote {\n margin: 0.5em 0;\n }\n\n // Remove margin on last element\n &:last-child {\n margin-bottom: 0;\n }\n\n // Nested lists\n ul,\n ol {\n margin: 0.5em 0 0.5em px2em(10px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2em(10px);\n margin-left: initial;\n }\n }\n }\n }\n\n // Definition lists\n dd {\n margin: 1em 0 1.5em px2em(30px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2em(30px);\n margin-left: initial;\n }\n }\n\n // Limit width to container, scale height proportionally\n img,\n svg {\n max-width: 100%;\n height: auto;\n\n // Left-aligned\n &[align=\"left\"] {\n margin: 1em;\n margin-left: 0;\n }\n\n // Right-aligned\n &[align=\"right\"] {\n margin: 1em;\n margin-right: 0;\n }\n\n // Remove top spacing of sole children\n &[align]:only-child {\n margin-top: 0;\n }\n }\n\n // Figures\n figure {\n width: fit-content;\n max-width: 100%;\n margin: 0 auto;\n text-align: center;\n }\n\n // Figure captions\n figcaption {\n max-width: px2rem(480px);\n margin: 0.5em auto 2em;\n font-style: italic;\n }\n\n // Limit width to container\n iframe {\n max-width: 100%;\n }\n\n // Data tables\n table:not([class]) {\n display: inline-block;\n max-width: 100%;\n overflow: auto;\n font-size: px2rem(12.8px);\n background: var(--md-default-bg-color);\n border-radius: px2rem(2px);\n box-shadow:\n 0 px2rem(4px) px2rem(10px) hsla(0, 0%, 0%, 0.05),\n 0 0 px2rem(1px) hsla(0, 0%, 0%, 0.1);\n touch-action: auto;\n\n // Reset display mode so table header wraps correctly when printing\n @media print {\n display: table;\n }\n\n // Due to margin collapse because of the necessary inline-block hack, we\n // cannot increase the bottom margin on the table, so we just increase the\n // top margin on the following element\n & + * {\n margin-top: 1.5em;\n }\n\n // Elements inside cells\n th > *,\n td > * {\n\n // Remove top spacing of first child\n &:first-child {\n margin-top: 0;\n }\n\n // Remove bottom spacing of last child\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Table headings and cells\n th:not([align]),\n td:not([align]) {\n text-align: left;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n text-align: right;\n }\n }\n\n // Table headings\n th {\n min-width: px2rem(100px);\n padding: px2em(12px, 12.8px) px2em(16px, 12.8px);\n color: var(--md-default-bg-color);\n vertical-align: top;\n background-color: var(--md-default-fg-color--light);\n\n // Links in table headings\n a {\n color: inherit;\n }\n }\n\n // Table cells\n td {\n padding: px2em(12px, 12.8px) px2em(16px, 12.8px);\n vertical-align: top;\n border-top: px2rem(1px) solid var(--md-default-fg-color--lightest);\n }\n\n // Table rows\n tr {\n transition: background-color 125ms;\n\n // Add background on hover\n &:hover {\n background-color: rgba(0, 0, 0, 0.035);\n box-shadow: 0 px2rem(1px) 0 var(--md-default-bg-color) inset;\n }\n\n // Remove top border on first row\n &:first-child td {\n border-top: 0;\n }\n }\n\n // Do not wrap links in tables\n a {\n word-break: normal;\n }\n }\n\n // Sortable tables\n table th[role=\"columnheader\"] {\n cursor: pointer;\n\n // Sort icon\n &::after {\n display: inline-block;\n width: 1.2em;\n height: 1.2em;\n margin-left: 0.5em;\n vertical-align: sub;\n mask-repeat: no-repeat;\n content: \" \";\n }\n\n // Sort ascending\n &[aria-sort=\"ascending\"]::after {\n background-color: currentColor;\n mask-image: var(--md-typeset-table--ascending);\n }\n\n // Sort descending\n &[aria-sort=\"descending\"]::after {\n background-color: currentColor;\n mask-image: var(--md-typeset-table--descending);\n }\n }\n\n // Wrapper for scrolling on overflow\n &__scrollwrap {\n margin: 1em px2rem(-16px);\n overflow-x: auto;\n touch-action: auto;\n }\n\n // Data table wrapper, in case JavaScript is available\n &__table {\n display: inline-block;\n margin-bottom: 0.5em;\n padding: 0 px2rem(16px);\n\n // Reset display mode so table header wraps correctly when printing\n @media print {\n display: block;\n }\n\n // Data tables\n html & table {\n display: table;\n width: 100%;\n margin: 0;\n overflow: hidden;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Variables\n// ----------------------------------------------------------------------------\n\n///\n/// Device-specific breakpoints\n///\n/// @example\n/// $break-devices: (\n/// mobile: (\n/// portrait: 220px 479px,\n/// landscape: 480px 719px\n/// ),\n/// tablet: (\n/// portrait: 720px 959px,\n/// landscape: 960px 1219px\n/// ),\n/// screen: (\n/// small: 1220px 1599px,\n/// medium: 1600px 1999px,\n/// large: 2000px\n/// )\n/// );\n///\n$break-devices: () !default;\n\n// ----------------------------------------------------------------------------\n// Helpers\n// ----------------------------------------------------------------------------\n\n///\n/// Choose minimum and maximum device widths\n///\n@function break-select-min-max($devices) {\n $min: 1000000;\n $max: 0;\n @each $key, $value in $devices {\n @while type-of($value) == map {\n $value: break-select-min-max($value);\n }\n @if type-of($value) == list {\n @each $number in $value {\n @if type-of($number) == number {\n $min: min($number, $min);\n @if $max != null {\n $max: max($number, $max);\n }\n } @else {\n @error \"Invalid number: #{$number}\";\n }\n }\n } @else if type-of($value) == number {\n $min: min($value, $min);\n $max: null;\n } @else {\n @error \"Invalid value: #{$value}\";\n }\n }\n @return $min, $max;\n}\n\n///\n/// Select minimum and maximum widths for a device breakpoint\n///\n@function break-select-device($device) {\n $current: $break-devices;\n @for $n from 1 through length($device) {\n @if type-of($current) == map {\n $current: map-get($current, nth($device, $n));\n } @else {\n @error \"Invalid device map: #{$devices}\";\n }\n }\n @if type-of($current) == list or type-of($current) == number {\n $current: (default: $current);\n }\n @return break-select-min-max($current);\n}\n\n// ----------------------------------------------------------------------------\n// Mixins\n// ----------------------------------------------------------------------------\n\n///\n/// A minimum-maximum media query breakpoint\n///\n@mixin break-at($breakpoint) {\n @if type-of($breakpoint) == number {\n @media screen and (min-width: $breakpoint) {\n @content;\n }\n } @else if type-of($breakpoint) == list {\n $min: nth($breakpoint, 1);\n $max: nth($breakpoint, 2);\n @if type-of($min) == number and type-of($max) == number {\n @media screen and (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n}\n\n///\n/// An orientation media query breakpoint\n///\n@mixin break-at-orientation($breakpoint) {\n @if type-of($breakpoint) == string {\n @media screen and (orientation: $breakpoint) {\n @content;\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n}\n\n///\n/// A maximum-aspect-ratio media query breakpoint\n///\n@mixin break-at-ratio($breakpoint) {\n @if type-of($breakpoint) == number {\n @media screen and (max-aspect-ratio: $breakpoint) {\n @content;\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n}\n\n///\n/// A minimum-maximum media query device breakpoint\n///\n@mixin break-at-device($device) {\n @if type-of($device) == string {\n $device: $device,;\n }\n @if type-of($device) == list {\n $breakpoint: break-select-device($device);\n @if nth($breakpoint, 2) != null {\n $min: nth($breakpoint, 1);\n $max: nth($breakpoint, 2);\n @media screen and (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n}\n\n///\n/// A minimum media query device breakpoint\n///\n@mixin break-from-device($device) {\n @if type-of($device) == string {\n $device: $device,;\n }\n @if type-of($device) == list {\n $breakpoint: break-select-device($device);\n $min: nth($breakpoint, 1);\n @media screen and (min-width: $min) {\n @content;\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n}\n\n///\n/// A maximum media query device breakpoint\n///\n@mixin break-to-device($device) {\n @if type-of($device) == string {\n $device: $device,;\n }\n @if type-of($device) == list {\n $breakpoint: break-select-device($device);\n $max: nth($breakpoint, 2);\n @media screen and (max-width: $max) {\n @content;\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n}\n","//\n// Name: Material Shadows\n// Description: Mixins for Material Design Shadows.\n// Version: 3.0.1\n//\n// Author: Denis Malinochkin\n// Git: https://github.com/mrmlnc/material-shadows\n//\n// twitter: @mrmlnc\n//\n// ------------------------------------\n\n\n// Mixins\n// ------------------------------------\n\n@mixin z-depth-transition() {\n transition: box-shadow .28s cubic-bezier(.4, 0, .2, 1);\n}\n\n@mixin z-depth-focus() {\n box-shadow: 0 0 8px rgba(0, 0, 0, .18), 0 8px 16px rgba(0, 0, 0, .36);\n}\n\n@mixin z-depth-2dp() {\n box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .14),\n 0 1px 5px 0 rgba(0, 0, 0, .12),\n 0 3px 1px -2px rgba(0, 0, 0, .2);\n}\n\n@mixin z-depth-3dp() {\n box-shadow: 0 3px 4px 0 rgba(0, 0, 0, .14),\n 0 1px 8px 0 rgba(0, 0, 0, .12),\n 0 3px 3px -2px rgba(0, 0, 0, .4);\n}\n\n@mixin z-depth-4dp() {\n box-shadow: 0 4px 5px 0 rgba(0, 0, 0, .14),\n 0 1px 10px 0 rgba(0, 0, 0, .12),\n 0 2px 4px -1px rgba(0, 0, 0, .4);\n}\n\n@mixin z-depth-6dp() {\n box-shadow: 0 6px 10px 0 rgba(0, 0, 0, .14),\n 0 1px 18px 0 rgba(0, 0, 0, .12),\n 0 3px 5px -1px rgba(0, 0, 0, .4);\n}\n\n@mixin z-depth-8dp() {\n box-shadow: 0 8px 10px 1px rgba(0, 0, 0, .14),\n 0 3px 14px 2px rgba(0, 0, 0, .12),\n 0 5px 5px -3px rgba(0, 0, 0, .4);\n}\n\n@mixin z-depth-16dp() {\n box-shadow: 0 16px 24px 2px rgba(0, 0, 0, .14),\n 0 6px 30px 5px rgba(0, 0, 0, .12),\n 0 8px 10px -5px rgba(0, 0, 0, .4);\n}\n\n@mixin z-depth-24dp() {\n box-shadow: 0 9px 46px 8px rgba(0, 0, 0, .14),\n 0 24px 38px 3px rgba(0, 0, 0, .12),\n 0 11px 15px -7px rgba(0, 0, 0, .4);\n}\n\n@mixin z-depth($dp: 2) {\n @if $dp == 2 {\n @include z-depth-2dp();\n } @else if $dp == 3 {\n @include z-depth-3dp();\n } @else if $dp == 4 {\n @include z-depth-4dp();\n } @else if $dp == 6 {\n @include z-depth-6dp();\n } @else if $dp == 8 {\n @include z-depth-8dp();\n } @else if $dp == 16 {\n @include z-depth-16dp();\n } @else if $dp == 24 {\n @include z-depth-24dp();\n }\n}\n\n\n// Class generator\n// ------------------------------------\n\n@mixin z-depth-classes($transition: false, $focus: false) {\n @if $transition == true {\n &-transition {\n @include z-depth-transition();\n }\n }\n\n @if $focus == true {\n &-focus {\n @include z-depth-focus();\n }\n }\n\n // The available values for the shadow depth\n @each $depth in 2, 3, 4, 6, 8, 16, 24 {\n &-#{$depth}dp {\n @include z-depth($depth);\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Variables\n// ----------------------------------------------------------------------------\n\n// Active (toggled) drawer\n$md-toggle__drawer--checked:\n \"[data-md-toggle=\\\"drawer\\\"]:checked ~\";\n\n// ----------------------------------------------------------------------------\n// Rules: base grid and containers\n// ----------------------------------------------------------------------------\n\n// Stretch container to viewport and set base font-sizefor simple calculations\n// based on relative ems (rems)\nhtml {\n height: 100%;\n // Hack: some browsers on some operating systems don't account for scroll\n // bars when firing media queries, so we need to do this for safety. This\n // currently impacts the table of contents component between 1220 and 1234px\n // and is to current knowledge not fixable.\n overflow-x: hidden;\n // Hack: normally, we would set the base font-size to 62.5%, so we can base\n // all calculations on 10px, but Chromium and Chrome define a minimal font\n // size of 12 if the system language is set to Chinese. For this reason we\n // just double the font-size, set it to 20px which seems to do the trick.\n //\n // See https://github.com/squidfunk/mkdocs-material/issues/911\n font-size: 125%;\n\n // [screen medium +]: Set base font-size to 11px\n @include break-from-device(screen medium) {\n font-size: 137.50%;\n }\n\n // [screen large +]: Set base font-size to 12px\n @include break-from-device(screen large) {\n font-size: 150%;\n }\n}\n\n// Stretch body to container and leave room for footer\nbody {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n min-height: 100%;\n // Hack: reset font-size to 10px, so the spacing for all inline elements is\n // correct again. Otherwise the spacing would be based on 20px.\n font-size: 0.5rem; // stylelint-disable-line unit-allowed-list\n background-color: var(--md-default-bg-color);\n\n // [tablet portrait -]: Lock body to disable scroll bubbling\n @include break-to-device(tablet portrait) {\n\n // Lock body to viewport height (e.g. in search mode)\n &[data-md-state=\"lock\"] {\n position: fixed;\n }\n }\n\n // Hack: we must not use flex, or Firefox will only print the first page\n // see https://mzl.la/39DgR3m\n @media print {\n display: block;\n }\n}\n\n// Horizontal separators\nhr {\n display: block;\n height: px2rem(1px);\n padding: 0;\n border: 0;\n}\n\n// Template-wide grid\n.md-grid {\n max-width: px2rem(1220px);\n margin-right: auto;\n margin-left: auto;\n}\n\n// Content wrapper\n.md-container {\n display: flex;\n flex-direction: column;\n flex-grow: 1;\n\n // Hack: we must not use flex, or Firefox will only print the first page\n // see https://mzl.la/39DgR3m\n @media print {\n display: block;\n }\n}\n\n// The main content should stretch to maximum height in the table\n.md-main {\n flex-grow: 1;\n\n // Increase top spacing of content area to give typography more room\n &__inner {\n display: flex;\n height: 100%;\n margin-top: px2rem(24px + 6px);\n }\n}\n\n// Apply ellipsis in case of overflowing text\n.md-ellipsis {\n display: block;\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n// ----------------------------------------------------------------------------\n// Rules: navigational elements\n// ----------------------------------------------------------------------------\n\n// Toggle checkbox\n.md-toggle {\n display: none;\n}\n\n// Overlay below expanded drawer\n.md-overlay {\n position: fixed;\n top: 0;\n z-index: 3;\n width: 0;\n height: 0;\n background-color: hsla(0, 0%, 0%, 0.54);\n opacity: 0;\n transition:\n width 0ms 250ms,\n height 0ms 250ms,\n opacity 250ms;\n\n // [tablet -]: Trigger overlay\n @include break-to-device(tablet) {\n\n // Expanded drawer\n #{$md-toggle__drawer--checked} & {\n width: 100%;\n height: 100%;\n opacity: 1;\n transition:\n width 0ms,\n height 0ms,\n opacity 250ms;\n }\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules: skip link\n// ----------------------------------------------------------------------------\n\n// Skip link\n.md-skip {\n position: fixed;\n // Hack: if we don't set the negative z-index, the skip link will induce the\n // creation of new layers when code blocks are near the header on scrolling\n z-index: -1;\n margin: px2rem(10px);\n padding: px2rem(6px) px2rem(10px);\n color: var(--md-default-bg-color);\n font-size: px2rem(12.8px);\n background-color: var(--md-default-fg-color);\n border-radius: px2rem(2px);\n transform: translateY(px2rem(8px));\n opacity: 0;\n\n // Show skip link on focus\n &:focus {\n z-index: 10;\n transform: translateY(0);\n opacity: 1;\n transition:\n transform 250ms cubic-bezier(0.4, 0, 0.2, 1),\n opacity 175ms 75ms;\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules: print styles\n// ----------------------------------------------------------------------------\n\n// Add margins to page\n@page {\n margin: 25mm;\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Announcement bar\n.md-announce {\n overflow: auto;\n background-color: var(--md-footer-bg-color);\n\n // Actual content\n &__inner {\n margin: px2rem(12px) auto;\n padding: 0 px2rem(16px);\n color: var(--md-footer-fg-color);\n font-size: px2rem(14px);\n }\n\n // Hide for print\n @media print {\n display: none;\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Button\n .md-button {\n display: inline-block;\n padding: px2em(10px) px2em(32px);\n color: var(--md-primary-fg-color);\n font-weight: 700;\n border: px2rem(2px) solid currentColor;\n border-radius: px2rem(2px);\n transition:\n color 125ms,\n background-color 125ms,\n border-color 125ms;\n\n // Primary button\n &--primary {\n color: var(--md-primary-bg-color);\n background-color: var(--md-primary-fg-color);\n border-color: var(--md-primary-fg-color);\n }\n\n // Focused or hovered button\n &:focus,\n &:hover {\n color: var(--md-accent-bg-color);\n background-color: var(--md-accent-fg-color);\n border-color: var(--md-accent-fg-color);\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-clipboard-icon: svg-load(\"@mdi/svg/svg/content-copy.svg\");\n}\n\n// ----------------------------------------------------------------------------\n\n// Copy to clipboard\n.md-clipboard {\n position: absolute;\n top: px2em(8px);\n right: px2em(8px);\n z-index: 1;\n width: px2em(24px);\n height: px2em(24px);\n color: var(--md-default-fg-color--lightest);\n // background-color: var(--md-code-bg-color);\n border-radius: px2rem(2px);\n cursor: pointer;\n transition: color 125ms;\n\n // Hide for print\n @media print {\n display: none;\n }\n\n // // Make room for clipboard button in case of overflowing code\n // .md-typeset & + code {\n // padding-right: px2em(24px + 2 * 8px, 13.6px);\n // }\n\n // Slightly smaller icon\n &::after {\n display: block;\n width: px2em(18px);\n height: px2em(18px);\n margin: 0 auto;\n background-color: currentColor;\n mask-image: var(--md-clipboard-icon);\n mask-repeat: no-repeat;\n content: \"\";\n }\n\n // Show on container hover\n pre:hover & {\n color: var(--md-default-fg-color--light);\n }\n\n // Focused or hovered icon\n pre &:focus,\n pre &:hover {\n color: var(--md-accent-fg-color);\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Content container\n.md-content {\n flex: 1;\n max-width: 100%;\n\n // [tablet landscape]: Decrease horizontal width\n @include break-at-device(tablet landscape) {\n max-width: calc(100% - #{px2rem(242px)});\n }\n\n // [screen +]: Decrease horizontal width\n @include break-from-device(screen) {\n max-width: calc(100% - #{px2rem(242px)} * 2);\n }\n\n // Define spacing\n &__inner {\n margin: 0 px2rem(16px) px2rem(24px);\n padding-top: px2rem(12px);\n\n // [screen +]: Increase horizontal spacing\n @include break-from-device(screen) {\n margin-right: px2rem(24px);\n margin-left: px2rem(24px);\n }\n\n // Hack: add pseudo element for spacing, as the overflow of the content\n // container may not be hidden due to an imminent offset error on targets\n &::before {\n display: block;\n height: px2rem(8px);\n content: \"\";\n }\n\n // Hack: remove bottom spacing of last element, due to margin collapse\n > :last-child {\n margin-bottom: 0;\n }\n }\n\n // Button next to the title\n &__button {\n float: right;\n margin: px2rem(8px) 0;\n margin-left: px2rem(8px);\n padding: 0;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n float: left;\n margin-right: px2rem(8px);\n margin-left: initial;\n\n // Flip icon vertically\n svg {\n transform: scaleX(-1);\n }\n }\n\n // Override default link color for icons\n .md-typeset & {\n color: var(--md-default-fg-color--lighter);\n }\n\n // Align text with icon\n svg {\n display: inline;\n vertical-align: top;\n }\n\n // Hide for print\n @media print {\n display: none;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Dialog rendered as snackbar\n.md-dialog {\n @include z-depth(2);\n\n position: fixed;\n right: px2rem(16px);\n bottom: px2rem(16px);\n left: initial;\n z-index: 2;\n display: block;\n min-width: px2rem(222px);\n padding: px2rem(8px) px2rem(12px);\n color: var(--md-default-bg-color);\n font-size: px2rem(14px);\n background: var(--md-default-fg-color);\n border: none;\n border-radius: px2rem(2px);\n transform: translateY(100%);\n opacity: 0;\n transition:\n transform 0ms 400ms,\n opacity 400ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: initial;\n left: px2rem(16px);\n }\n\n // Show open dialog\n &[data-md-state=\"open\"] {\n transform: translateY(0);\n opacity: 1;\n transition:\n transform 400ms cubic-bezier(0.075, 0.85, 0.175, 1),\n opacity 400ms;\n }\n\n // Hide for print\n @media print {\n display: none;\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Application header (stays always on top)\n.md-header {\n position: sticky;\n top: 0;\n right: 0;\n left: 0;\n z-index: 2;\n height: px2rem(48px);\n color: var(--md-primary-bg-color);\n background-color: var(--md-primary-fg-color);\n // Hack: reduce jitter by adding a transparent box shadow of the same size\n // so the size of the layer doesn't change during animation\n box-shadow:\n 0 0 px2rem(4px) rgba(0, 0, 0, 0),\n 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0);\n transition:\n color 250ms,\n background-color 250ms;\n\n // Always hide shadow, in case JavaScript is not available\n .no-js & {\n box-shadow: none;\n transition: none;\n }\n\n // Show and animate shadow\n &[data-md-state=\"shadow\"] {\n box-shadow:\n 0 0 px2rem(4px) rgba(0, 0, 0, 0.1),\n 0 px2rem(4px) px2rem(8px) rgba(0, 0, 0, 0.2);\n transition:\n color 250ms,\n background-color 250ms,\n box-shadow 250ms;\n }\n\n // Hide for print\n @media print {\n display: none;\n }\n}\n\n// Navigation within header\n.md-header-nav {\n display: flex;\n padding: 0 px2rem(4px);\n\n // Icon buttons\n &__button {\n position: relative;\n z-index: 1;\n display: block;\n margin: px2rem(4px);\n padding: px2rem(8px);\n cursor: pointer;\n transition: opacity 250ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n\n // Flip icon vertically\n svg {\n transform: scaleX(-1);\n }\n }\n\n // Focused or hovered icon\n &:focus,\n &:hover {\n opacity: 0.7;\n }\n\n // Logo\n &.md-logo {\n margin: px2rem(4px);\n padding: px2rem(8px);\n\n // Image or icon\n img,\n svg {\n display: block;\n width: px2rem(24px);\n height: px2rem(24px);\n fill: currentColor;\n }\n }\n\n // Hide search icon, if JavaScript is not available.\n .no-js &[for=\"__search\"] {\n display: none;\n }\n\n // [tablet landscape +]: Hide the search button\n @include break-from-device(tablet landscape) {\n\n // Search button\n &[for=\"__search\"] {\n display: none;\n }\n }\n\n // [tablet -]: Hide the logo\n @include break-to-device(tablet) {\n\n // Logo\n &.md-logo {\n display: none;\n }\n }\n\n // [screen +]: Hide the menu button\n @include break-from-device(screen) {\n\n // Menu button\n &[for=\"__drawer\"] {\n display: none;\n }\n }\n }\n\n // Header topics\n &__topic {\n position: absolute;\n width: 100%;\n transition:\n transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),\n opacity 150ms;\n\n // Page title\n & + & {\n z-index: -1;\n transform: translateX(px2rem(25px));\n opacity: 0;\n transition:\n transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),\n opacity 150ms;\n pointer-events: none;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n transform: translateX(px2rem(-25px));\n }\n }\n\n // Induce ellipsis, if no JavaScript is available\n .no-js & {\n position: initial;\n }\n\n // Hide page title as it is invisible anyway and will overflow the header\n .no-js & + & {\n display: none;\n }\n }\n\n // Header title - set line height to match icon for correct alignment\n &__title {\n flex-grow: 1;\n padding: 0 px2rem(20px);\n font-size: px2rem(18px);\n line-height: px2rem(48px);\n\n // Show page title\n &[data-md-state=\"active\"] .md-header-nav__topic {\n z-index: -1;\n transform: translateX(px2rem(-25px));\n opacity: 0;\n transition:\n transform 400ms cubic-bezier(1, 0.7, 0.1, 0.1),\n opacity 150ms;\n pointer-events: none;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n transform: translateX(px2rem(25px));\n }\n\n // Page title\n & + .md-header-nav__topic {\n z-index: 0;\n transform: translateX(0);\n opacity: 1;\n transition:\n transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),\n opacity 150ms;\n pointer-events: initial;\n }\n }\n\n // Patch ellipsis\n > .md-header-nav__ellipsis {\n position: relative;\n width: 100%;\n height: 100%;\n }\n }\n\n // Repository containing source\n &__source {\n display: none;\n\n // [tablet landscape +]: Show the reposistory from tablet\n @include break-from-device(tablet landscape) {\n display: block;\n width: px2rem(234px);\n max-width: px2rem(234px);\n margin-left: px2rem(20px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2rem(20px);\n margin-left: initial;\n }\n }\n\n // [screen +]: Increase spacing of search bar\n @include break-from-device(screen) {\n margin-left: px2rem(28px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2rem(28px);\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Application footer\n.md-footer {\n color: var(--md-footer-fg-color);\n background-color: var(--md-footer-bg-color);\n\n // Hide for print\n @media print {\n display: none;\n }\n}\n\n// Navigation within footer\n.md-footer-nav {\n\n // Set spacing\n &__inner {\n padding: px2rem(4px);\n overflow: auto;\n }\n\n // Links to previous and next page\n &__link {\n display: flex;\n padding-top: px2rem(28px);\n padding-bottom: px2rem(8px);\n transition: opacity 250ms;\n\n // [tablet +]: Set proportional width\n @include break-from-device(tablet) {\n width: 50%;\n }\n\n // Focused or hovered links\n &:focus,\n &:hover {\n opacity: 0.7;\n }\n\n // Link to previous page\n &--prev {\n float: left;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n float: right;\n\n // Flip icon vertically\n svg {\n transform: scaleX(-1);\n }\n }\n\n // [mobile -]: Hide title for previous page\n @include break-to-device(mobile) {\n width: 25%;\n\n // Title\n .md-footer-nav__title {\n display: none;\n }\n }\n }\n\n // Link to next page\n &--next {\n float: right;\n text-align: right;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n float: left;\n text-align: left;\n\n // Flip icon vertically\n svg {\n transform: scaleX(-1);\n }\n }\n\n // [mobile -]: Hide title for previous page\n @include break-to-device(mobile) {\n width: 75%;\n }\n }\n }\n\n // Link title - set line height to match icon for correct alignment\n &__title {\n position: relative;\n flex-grow: 1;\n max-width: calc(100% - #{px2rem(48px)});\n padding: 0 px2rem(20px);\n font-size: px2rem(18px);\n line-height: px2rem(48px);\n }\n\n // Link button\n &__button {\n margin: px2rem(4px);\n padding: px2rem(8px);\n }\n\n // Link direction\n &__direction {\n position: absolute;\n right: 0;\n left: 0;\n margin-top: px2rem(-20px);\n padding: 0 px2rem(20px);\n font-size: px2rem(12.8px);\n opacity: 0.7;\n }\n}\n\n// Non-navigational information\n.md-footer-meta {\n background-color: var(--md-footer-bg-color--dark);\n\n // Set spacing\n &__inner {\n display: flex;\n flex-wrap: wrap;\n justify-content: space-between;\n padding: px2rem(4px);\n }\n\n // Use a decent color for non-hovered links and ensure specificity\n html &.md-typeset a {\n color: var(--md-footer-fg-color--light);\n\n // Focused or hovered link\n &:focus,\n &:hover {\n color: var(--md-footer-fg-color);\n }\n }\n}\n\n// Copyright and theme information\n.md-footer-copyright {\n width: 100%;\n margin: auto px2rem(12px);\n padding: px2rem(8px) 0;\n color: var(--md-footer-fg-color--lighter);\n font-size: px2rem(12.8px);\n\n // [tablet portrait +]: Show next to social media links\n @include break-from-device(tablet portrait) {\n width: auto;\n }\n\n // Highlight copyright information\n &__highlight {\n color: var(--md-footer-fg-color--light);\n }\n}\n\n// Social links\n.md-footer-social {\n margin: 0 px2rem(8px);\n padding: px2rem(4px) 0 px2rem(12px);\n\n // [tablet portrait +]: Show next to copyright information\n @include break-from-device(tablet portrait) {\n padding: px2rem(12px) 0;\n }\n\n // Link with icon\n &__link {\n display: inline-block;\n width: px2rem(32px);\n height: px2rem(32px);\n text-align: center;\n\n // Adjust line-height to match height for correct alignment\n &::before {\n line-height: 1.9;\n }\n\n // Social icon\n svg {\n max-height: px2rem(16px);\n vertical-align: -25%;\n fill: currentColor;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-nav-icon--prev: svg-load(\"@mdi/svg/svg/arrow-left.svg\");\n --md-nav-icon--next: svg-load(\"@mdi/svg/svg/chevron-right.svg\");\n --md-toc-icon: svg-load(\"@mdi/svg/svg/table-of-contents.svg\");\n}\n\n// ----------------------------------------------------------------------------\n\n// Navigation container\n.md-nav {\n font-size: px2rem(14px);\n line-height: 1.3;\n\n // List title\n &__title {\n display: block;\n padding: 0 px2rem(12px);\n overflow: hidden;\n font-weight: 700;\n text-overflow: ellipsis;\n\n // Hide buttons by default\n .md-nav__button {\n display: none;\n\n // Stretch images\n img {\n width: 100%;\n height: auto;\n }\n\n // Logo\n &.md-logo {\n\n // Image or icon\n img,\n svg {\n display: block;\n width: px2rem(48px);\n height: px2rem(48px);\n }\n\n // Icon\n svg {\n fill: currentColor;\n }\n }\n }\n }\n\n // List of items\n &__list {\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n // List item\n &__item {\n padding: 0 px2rem(12px);\n\n // Add bottom spacing to last item\n &:last-child {\n padding-bottom: px2rem(12px);\n }\n\n // 2nd+ level items\n & & {\n padding-right: 0;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(12px);\n padding-left: 0;\n }\n\n // Remove bottom spacing for nested items\n &:last-child {\n padding-bottom: 0;\n }\n }\n }\n\n // Link inside item\n &__link {\n display: block;\n margin-top: 0.625em;\n overflow: hidden;\n text-overflow: ellipsis;\n cursor: pointer;\n transition: color 125ms;\n scroll-snap-align: start;\n\n // Hide link to table of contents by default - this will only match the\n // table of contents inside the drawer below and including tablet portrait\n html &[for=\"__toc\"] {\n display: none;\n\n // Hide table of contents by default\n & ~ .md-nav {\n display: none;\n }\n }\n\n // Blurred link\n &[data-md-state=\"blur\"] {\n color: var(--md-default-fg-color--light);\n }\n\n // Active link\n .md-nav__item &--active {\n color: var(--md-typeset-a-color);\n }\n\n // Reset active color for nested list titles\n .md-nav__item--nested > & {\n color: inherit;\n }\n\n // Focused or hovered link\n &:focus,\n &:hover {\n color: var(--md-accent-fg-color);\n }\n }\n\n // Repository containing source\n &__source {\n display: none;\n }\n\n // [tablet -]: Layered navigation\n @include break-to-device(tablet) {\n background-color: var(--md-default-bg-color);\n\n // Stretch primary navigation to drawer\n &--primary,\n &--primary .md-nav {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n z-index: 1;\n display: flex;\n flex-direction: column;\n height: 100%;\n }\n\n // Adjust styles for primary navigation\n &--primary {\n\n // List title and item\n .md-nav__title,\n .md-nav__item {\n font-size: px2rem(16px);\n line-height: 1.5;\n }\n\n // List title\n .md-nav__title {\n position: relative;\n height: px2rem(112px);\n padding: px2rem(60px) px2rem(16px) px2rem(4px);\n color: var(--md-default-fg-color--light);\n font-weight: 400;\n line-height: px2rem(48px);\n white-space: nowrap;\n background-color: var(--md-default-fg-color--lightest);\n cursor: pointer;\n\n // Navigation icon\n .md-nav__icon {\n position: absolute;\n top: px2rem(8px);\n left: px2rem(8px);\n display: block;\n width: px2rem(24px);\n height: px2rem(24px);\n margin: px2rem(4px);\n\n // Previous navigation item icon\n &::after {\n display: block;\n width: 100%;\n height: 100%;\n background-color: currentColor;\n mask-image: var(--md-nav-icon--prev);\n mask-repeat: no-repeat;\n content: \"\";\n }\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2rem(8px);\n left: initial;\n }\n }\n\n // Main lists\n ~ .md-nav__list {\n overflow-y: auto;\n background-color: var(--md-default-bg-color);\n box-shadow:\n 0 px2rem(1px) 0 var(--md-default-fg-color--lightest) inset;\n scroll-snap-type: y mandatory;\n touch-action: pan-y;\n\n // Remove border for first list item\n > .md-nav__item:first-child {\n border-top: 0;\n }\n }\n\n // Site title in main navigation\n &[for=\"__drawer\"] {\n position: relative;\n color: var(--md-primary-bg-color);\n background-color: var(--md-primary-fg-color);\n\n // Site logo\n .md-nav__button {\n position: absolute;\n top: px2rem(4px);\n left: px2rem(4px);\n display: block;\n margin: px2rem(4px);\n padding: px2rem(8px);\n font-size: px2rem(48px);\n }\n }\n }\n\n // Adjust for right-to-left languages\n html [dir=\"rtl\"] & .md-nav__title {\n\n // Site title in main navigation\n &[for=\"__drawer\"] .md-nav__button {\n right: px2rem(4px);\n left: initial;\n }\n }\n\n // List of items\n .md-nav__list {\n flex: 1;\n }\n\n // List item\n .md-nav__item {\n padding: 0;\n border-top: px2rem(1px) solid var(--md-default-fg-color--lightest);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding: 0;\n }\n\n // Increase spacing to account for icon\n &--nested > .md-nav__link {\n padding-right: px2rem(48px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(16px);\n padding-left: px2rem(48px);\n }\n }\n\n // Active parent item\n &--active > .md-nav__link {\n color: var(--md-typeset-a-color);\n\n // Focused or hovered linl\n &:focus,\n &:hover {\n color: var(--md-accent-fg-color);\n }\n }\n }\n\n // Link inside item\n .md-nav__link {\n position: relative;\n margin-top: 0;\n padding: px2rem(12px) px2rem(16px);\n\n // Navigation icon\n .md-nav__icon {\n position: absolute;\n top: 50%;\n right: px2rem(12px);\n width: px2rem(24px);\n height: px2rem(24px);\n margin-top: px2rem(-12px);\n color: inherit;\n font-size: px2rem(24px);\n\n // Next navigation item icon\n &::after {\n display: block;\n width: 100%;\n height: 100%;\n background-color: currentColor;\n mask-image: var(--md-nav-icon--next);\n mask-repeat: no-repeat;\n content: \"\";\n }\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: initial;\n left: px2rem(12px);\n }\n }\n }\n\n // Flip icon vertically\n .md-nav__icon::after {\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n transform: scale(-1);\n }\n }\n\n // Table of contents inside navigation\n .md-nav--secondary {\n\n // Set links to static to avoid unnecessary layering\n .md-nav__link {\n position: static;\n }\n\n // Set nested navigation for table of contents to static\n .md-nav {\n position: static;\n background-color: transparent;\n\n // 3rd level link\n .md-nav__link {\n padding-left: px2rem(28px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(28px);\n padding-left: initial;\n }\n }\n\n // 4th level link\n .md-nav .md-nav__link {\n padding-left: px2rem(40px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(40px);\n padding-left: initial;\n }\n }\n\n // 5th level link\n .md-nav .md-nav .md-nav__link {\n padding-left: px2rem(52px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(52px);\n padding-left: initial;\n }\n }\n\n // 6th level link\n .md-nav .md-nav .md-nav .md-nav__link {\n padding-left: px2rem(64px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(64px);\n padding-left: initial;\n }\n }\n }\n }\n }\n\n // Hide nested navigation by default\n .md-nav__toggle ~ & {\n display: flex;\n transform: translateX(100%);\n opacity: 0;\n transition:\n transform 250ms cubic-bezier(0.8, 0, 0.6, 1),\n opacity 125ms 50ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n transform: translateX(-100%);\n }\n }\n\n // Expand nested navigation, if toggle is checked\n .md-nav__toggle:checked ~ & {\n transform: translateX(0);\n opacity: 1;\n transition:\n transform 250ms cubic-bezier(0.4, 0, 0.2, 1),\n opacity 125ms 125ms;\n\n // Hack: reduce jitter\n > .md-nav__list {\n backface-visibility: hidden;\n }\n }\n }\n\n // [tablet portrait -]: Show table of contents in drawer\n @include break-to-device(tablet portrait) {\n\n // Show link to table of contents - higher specificity is necessary to\n // display the table of contents inside the drawer\n html &__link[for=\"__toc\"] {\n display: block;\n padding-right: px2rem(48px);\n\n // Hide link to current item\n + .md-nav__link {\n display: none;\n }\n\n // Table of contents icon\n .md-icon::after {\n display: block;\n width: 100%;\n height: 100%;\n mask-image: var(--md-toc-icon);\n background-color: currentColor;\n content: \"\";\n }\n\n // Show table of contents\n & ~ .md-nav {\n display: flex;\n }\n }\n\n // Adjust for right-to-left languages\n html [dir=\"rtl\"] &__link {\n padding-right: px2rem(16px);\n padding-left: px2rem(48px);\n }\n\n // Repository containing source\n &__source {\n display: block;\n padding: 0 px2rem(4px);\n color: var(--md-primary-bg-color);\n background-color: var(--md-primary-fg-color--dark);\n }\n }\n\n // [tablet landscape +]: Tree-like navigation\n @include break-from-device(tablet landscape) {\n\n // List title\n &--secondary .md-nav__title {\n\n // Snap to table of contents title\n &[for=\"__toc\"] {\n scroll-snap-align: start;\n }\n\n // Hide icon\n .md-nav__icon {\n display: none;\n }\n }\n }\n\n // [screen +]: Tree-like navigation\n @include break-from-device(screen) {\n transition: max-height 250ms cubic-bezier(0.86, 0, 0.07, 1);\n\n // List title\n &--primary .md-nav__title {\n\n // Snap to site title\n &[for=\"__drawer\"] {\n scroll-snap-align: start;\n }\n\n // Hide icon\n .md-nav__icon {\n display: none;\n }\n }\n\n // Hide nested navigation by default\n .md-nav__toggle ~ & {\n display: none;\n }\n\n // Show nested navigation, if toggle is checked\n .md-nav__toggle:checked ~ & {\n display: block;\n }\n\n // Hide titles for nested navigation\n &__item--nested > .md-nav > &__title {\n display: none;\n }\n\n // Navigation icon\n &__icon {\n float: right;\n width: px2rem(18px);\n height: px2rem(18px);\n transition: transform 250ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n float: left;\n transform: rotate(180deg);\n }\n\n // Inline icon and adjust icon to match font size\n &::after {\n display: inline-block;\n width: 100%;\n height: 100%;\n vertical-align: px2rem(-2px);\n background-color: currentColor;\n mask-image: var(--md-nav-icon--next);\n mask-repeat: no-repeat;\n content: \"\";\n }\n\n // Rotate icon for expanded lists\n .md-nav__item--nested .md-nav__toggle:checked ~ .md-nav__link & {\n transform: rotate(90deg);\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Variables\n// ----------------------------------------------------------------------------\n\n// Active (toggled) search\n$md-toggle__search--checked:\n \"[data-md-toggle=\\\"search\\\"]:checked ~ .md-header\";\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-search-result-icon: svg-load(\"@mdi/svg/svg/file-search-outline.svg\");\n}\n\n// ----------------------------------------------------------------------------\n\n// Search container\n.md-search {\n position: relative;\n\n // Hide search, if JavaScript is not available.\n .no-js & {\n display: none;\n }\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n padding: px2rem(4px) 0;\n }\n\n // Search modal overlay\n &__overlay {\n z-index: 1;\n opacity: 0;\n\n // [tablet portrait -]: Full-screen search bar\n @include break-to-device(tablet portrait) {\n position: absolute;\n top: px2rem(4px);\n left: px2rem(-44px);\n width: px2rem(40px);\n height: px2rem(40px);\n overflow: hidden;\n background-color: var(--md-default-bg-color);\n border-radius: px2rem(20px);\n transform-origin: center;\n transition:\n transform 300ms 100ms,\n opacity 200ms 200ms;\n pointer-events: none;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2rem(-44px);\n left: initial;\n }\n\n // Expanded overlay\n #{$md-toggle__search--checked} & {\n opacity: 1;\n transition:\n transform 400ms,\n opacity 100ms;\n }\n }\n\n // Set scale factors\n #{$md-toggle__search--checked} & {\n\n // [mobile portrait -]: Scale up 45 times\n @include break-to-device(mobile portrait) {\n transform: scale(45);\n }\n\n // [mobile landscape]: Scale up 60 times\n @include break-at-device(mobile landscape) {\n transform: scale(60);\n }\n\n // [tablet portrait]: Scale up 75 times\n @include break-at-device(tablet portrait) {\n transform: scale(75);\n }\n }\n\n // [tablet landscape +]: Overlay for better focus on search\n @include break-from-device(tablet landscape) {\n position: fixed;\n top: 0;\n left: 0;\n width: 0;\n height: 0;\n background-color: hsla(0, 0%, 0%, 0.54);\n cursor: pointer;\n transition:\n width 0ms 250ms,\n height 0ms 250ms,\n opacity 250ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: 0;\n left: initial;\n }\n\n // Expanded overlay\n #{$md-toggle__search--checked} & {\n width: 100%;\n height: 100%;\n opacity: 1;\n transition:\n width 0ms,\n height 0ms,\n opacity 250ms;\n }\n }\n }\n\n // Search modal wrapper\n &__inner {\n // Hack: reduce jitter\n backface-visibility: hidden;\n\n // [tablet portrait -]: Put search modal off-canvas by default\n @include break-to-device(tablet portrait) {\n position: fixed;\n top: 0;\n left: 100%;\n z-index: 2;\n width: 100%;\n height: 100%;\n transform: translateX(5%);\n opacity: 0;\n transition:\n right 0ms 300ms,\n left 0ms 300ms,\n transform 150ms 150ms cubic-bezier(0.4, 0, 0.2, 1),\n opacity 150ms 150ms;\n\n // Active search modal\n #{$md-toggle__search--checked} & {\n left: 0;\n transform: translateX(0);\n opacity: 1;\n transition:\n right 0ms 0ms,\n left 0ms 0ms,\n transform 150ms 150ms cubic-bezier(0.1, 0.7, 0.1, 1),\n opacity 150ms 150ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: 0;\n left: initial;\n }\n }\n\n // Adjust for right-to-left languages\n html [dir=\"rtl\"] & {\n right: 100%;\n left: initial;\n transform: translateX(-5%);\n }\n }\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n position: relative;\n float: right;\n width: px2rem(234px);\n padding: px2rem(2px) 0;\n transition: width 250ms cubic-bezier(0.1, 0.7, 0.1, 1);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n float: left;\n }\n }\n\n // Set maximum width\n #{$md-toggle__search--checked} & {\n\n // [tablet landscape]: Do not overlay title\n @include break-at-device(tablet landscape) {\n width: px2rem(468px);\n }\n\n // [screen +]: Match content width\n @include break-from-device(screen) {\n width: px2rem(688px);\n }\n }\n }\n\n // Search form\n &__form {\n position: relative;\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n border-radius: px2rem(2px);\n }\n }\n\n // Search input\n &__input {\n position: relative;\n z-index: 2;\n padding: 0 px2rem(44px) 0 px2rem(72px);\n text-overflow: ellipsis;\n background-color: var(--md-default-bg-color);\n transition:\n color 250ms,\n background-color 250ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding: 0 px2rem(72px) 0 px2rem(44px);\n }\n\n // Transition on placeholder\n &::placeholder {\n transition: color 250ms;\n }\n\n // Placeholder and icon color in active state\n ~ .md-search__icon,\n &::placeholder {\n color: var(--md-default-fg-color--light);\n }\n\n // Remove the \"x\" rendered by Internet Explorer\n &::-ms-clear {\n display: none;\n }\n\n // [tablet portrait -]: Full-screen search bar\n @include break-to-device(tablet portrait) {\n width: 100%;\n height: px2rem(48px);\n font-size: px2rem(18px);\n }\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n width: 100%;\n height: px2rem(36px);\n padding-left: px2rem(44px);\n color: inherit;\n font-size: px2rem(16px);\n background-color: hsla(0, 0%, 0%, 0.26);\n border-radius: px2rem(2px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(44px);\n }\n\n // Search icon color\n + .md-search__icon {\n color: var(--md-primary-bg-color);\n }\n\n // Placeholder color\n &::placeholder {\n color: var(--md-primary-bg-color--light);\n }\n\n // Hovered search field\n &:hover {\n background-color: hsla(0, 0%, 100%, 0.12);\n }\n\n // Set light background on active search field\n #{$md-toggle__search--checked} & {\n color: var(--md-default-fg-color);\n text-overflow: clip;\n background-color: var(--md-default-bg-color);\n border-radius: px2rem(2px) px2rem(2px) 0 0;\n\n // Search icon and placeholder color in active state\n + .md-search__icon,\n &::placeholder {\n color: var(--md-default-fg-color--light);\n }\n }\n }\n }\n\n // Search icon\n &__icon {\n position: absolute;\n z-index: 2;\n width: px2rem(24px);\n height: px2rem(24px);\n cursor: pointer;\n transition:\n color 250ms,\n opacity 250ms;\n\n // Hovered icon\n &:hover {\n opacity: 0.7;\n }\n\n // Search icon\n &[for=\"__search\"] {\n top: px2rem(6px);\n left: px2rem(10px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2rem(10px);\n left: initial;\n\n // Flip icon vertically\n svg {\n transform: scaleX(-1);\n }\n }\n\n // [tablet portrait -]: Full-screen search bar\n @include break-to-device(tablet portrait) {\n top: px2rem(12px);\n left: px2rem(16px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2rem(16px);\n left: initial;\n }\n\n // Hide the magnifying glass (1st icon)\n svg:first-child {\n display: none;\n }\n }\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n pointer-events: none;\n\n // Hide the arrow (2nd icon)\n svg:last-child {\n display: none;\n }\n }\n }\n\n // Reset button\n &[type=\"reset\"] {\n top: px2rem(6px);\n right: px2rem(10px);\n transform: scale(0.75);\n opacity: 0;\n transition:\n transform 150ms cubic-bezier(0.1, 0.7, 0.1, 1),\n opacity 150ms;\n pointer-events: none;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: initial;\n left: px2rem(10px);\n }\n\n // [tablet portrait -]: Full-screen search bar\n @include break-to-device(tablet portrait) {\n top: px2rem(12px);\n right: px2rem(16px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: initial;\n left: px2rem(16px);\n }\n }\n\n // Show reset button if search is active and input non-empty\n #{$md-toggle__search--checked}\n .md-search__input:not(:placeholder-shown) ~ & {\n transform: scale(1);\n opacity: 1;\n pointer-events: initial;\n\n // Hovered icon\n &:hover {\n opacity: 0.7;\n }\n }\n }\n }\n\n // Search output container\n &__output {\n position: absolute;\n z-index: 1;\n width: 100%;\n overflow: hidden;\n border-radius: 0 0 px2rem(2px) px2rem(2px);\n\n // [tablet portrait -]: Full-screen search bar\n @include break-to-device(tablet portrait) {\n top: px2rem(48px);\n bottom: 0;\n }\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n top: px2rem(38px);\n opacity: 0;\n transition: opacity 400ms;\n\n // Show search output in active state\n #{$md-toggle__search--checked} & {\n @include z-depth(6);\n\n opacity: 1;\n }\n }\n }\n\n // Wrapper for scrolling on overflow\n &__scrollwrap {\n height: 100%;\n overflow-y: auto;\n background-color: var(--md-default-bg-color);\n // Hack: reduce jitter\n backface-visibility: hidden;\n scroll-snap-type: y mandatory;\n touch-action: pan-y;\n\n // Mitigiate excessive repaints on non-retina devices\n @media (max-resolution: 1dppx) {\n transform: translateZ(0);\n }\n\n // [tablet landscape]: Set absolute width to omit unnecessary reflow\n @include break-at-device(tablet landscape) {\n width: px2rem(468px);\n }\n\n // [screen +]: Set absolute width to omit unnecessary reflow\n @include break-from-device(screen) {\n width: px2rem(688px);\n }\n\n // [tablet landscape +]: Limit height to viewport\n @include break-from-device(tablet landscape) {\n max-height: 0;\n // Override Firefox scrollbar style\n scrollbar-width: thin;\n scrollbar-color: var(--md-default-fg-color--lighter) transparent;\n\n // Expand in active state\n #{$md-toggle__search--checked} & {\n max-height: 75vh;\n }\n\n // Override Firefox scrollbar hover color\n &:hover {\n scrollbar-color: var(--md-accent-fg-color) transparent;\n }\n\n // Override native scrollbar styles\n &::-webkit-scrollbar {\n width: px2rem(4px);\n height: px2rem(4px);\n }\n\n // Scrollbar thumb\n &::-webkit-scrollbar-thumb {\n background-color: var(--md-default-fg-color--lighter);\n\n // Hovered scrollbar thumb\n &:hover {\n background-color: var(--md-accent-fg-color);\n }\n }\n }\n }\n}\n\n// Search result\n.md-search-result {\n color: var(--md-default-fg-color);\n word-break: break-word;\n\n // Search metadata\n &__meta {\n padding: 0 px2rem(16px);\n color: var(--md-default-fg-color--light);\n font-size: px2rem(12.8px);\n line-height: px2rem(36px);\n background-color: var(--md-default-fg-color--lightest);\n scroll-snap-align: start;\n\n // [tablet landscape +]: Increase left indent\n @include break-from-device(tablet landscape) {\n padding-left: px2rem(44px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(44px);\n padding-left: initial;\n }\n }\n }\n\n // List of items\n &__list {\n margin: 0;\n padding: 0;\n list-style: none;\n }\n\n // List item\n &__item {\n box-shadow: 0 px2rem(-1px) 0 var(--md-default-fg-color--lightest);\n\n // No border for first item\n &:first-child {\n box-shadow: none;\n }\n }\n\n // Link inside item\n &__link {\n display: block;\n outline: none;\n transition: background 250ms;\n scroll-snap-align: start;\n\n // Focused or hovered link\n &:focus,\n &:hover {\n background-color: var(--md-accent-fg-color--transparent);\n\n // Slightly transparent icon\n .md-search-result__article::before {\n opacity: 0.7;\n }\n }\n\n // Add a little spacing on the last element of the last link\n &:last-child p:last-child {\n margin-bottom: px2rem(12px);\n }\n }\n\n // Search result container\n &__more summary {\n display: block;\n padding: px2em(12px) px2rem(16px);\n color: var(--md-typeset-a-color);\n font-size: px2rem(12.8px);\n outline: 0;\n cursor: pointer;\n transition:\n color 250ms,\n background-color 250ms;\n scroll-snap-align: start;\n\n // Focused or hovered button\n &:focus,\n &:hover {\n color: var(--md-accent-fg-color);\n background-color: var(--md-accent-fg-color--transparent);\n }\n\n // [tablet landscape +]: Increase left indent\n @include break-from-device(tablet landscape) {\n padding-left: px2rem(44px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(44px);\n padding-left: px2rem(16px);\n }\n }\n\n // Remove default details marker\n &::-webkit-details-marker {\n display: none;\n }\n\n // All following elements\n & ~ * {\n\n // Make less relevant terms more transparent\n > * {\n opacity: 0.65;\n }\n }\n }\n\n // Article - document or section\n &__article {\n position: relative;\n padding: 0 px2rem(16px);\n overflow: hidden;\n\n // [tablet landscape +]: Increase left indent\n @include break-from-device(tablet landscape) {\n padding-left: px2rem(44px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding-right: px2rem(44px);\n padding-left: px2rem(16px);\n }\n }\n\n // Document\n &--document {\n\n // Title\n .md-search-result__title {\n margin: px2rem(11px) 0;\n font-weight: 400;\n font-size: px2rem(16px);\n line-height: 1.4;\n }\n }\n }\n\n // Search result icon\n &__icon {\n position: absolute;\n left: 0;\n width: px2rem(24px);\n height: px2rem(24px);\n margin: px2rem(10px);\n color: var(--md-default-fg-color--light);\n\n // Inline icon and adjust icon to match font size\n &::after {\n display: inline-block;\n width: 100%;\n height: 100%;\n background-color: currentColor;\n mask-image: var(--md-search-result-icon);\n mask-repeat: no-repeat;\n content: \"\";\n }\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: 0;\n left: initial;\n\n // Flip icon vertically\n &::after {\n transform: scaleX(-1);\n }\n }\n\n // [tablet portrait -]: Hide page icon\n @include break-to-device(tablet portrait) {\n display: none;\n }\n }\n\n // Title\n &__title {\n margin: 0.5em 0;\n font-weight: 700;\n font-size: px2rem(12.8px);\n line-height: 1.6;\n }\n\n // Teaser\n &__teaser {\n display: -webkit-box;\n max-height: px2rem(40px);\n margin: 0.5em 0;\n overflow: hidden;\n color: var(--md-default-fg-color--light);\n font-size: px2rem(12.8px);\n line-height: 1.6;\n text-overflow: ellipsis;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n\n // [mobile -]: Increase number of lines\n @include break-to-device(mobile) {\n max-height: px2rem(60px);\n -webkit-line-clamp: 3;\n }\n\n // [tablet landscape]: Increase number of lines\n @include break-at-device(tablet landscape) {\n max-height: px2rem(60px);\n -webkit-line-clamp: 3;\n }\n\n // Search term highlighting\n mark {\n background-color: transparent;\n border-bottom: px2rem(1px) solid var(--md-accent-fg-color);\n }\n }\n\n // Terms\n &__terms {\n margin: 0.5em 0;\n font-size: px2rem(12.8px);\n font-style: italic;\n }\n\n // Search term highlighting\n mark {\n color: var(--md-accent-fg-color);\n background-color: transparent;\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Variables\n// ----------------------------------------------------------------------------\n\n// Active (toggled) drawer\n$md-toggle__drawer--checked:\n \"[data-md-toggle=\\\"drawer\\\"]:checked ~ .md-container\";\n\n// ----------------------------------------------------------------------------\n// Keyframes\n// ----------------------------------------------------------------------------\n\n// Activate scroll snapping with delay\n@keyframes md-sidebar__scrollwrap--hack {\n 0%, 99% {\n scroll-snap-type: none;\n }\n\n 100% {\n scroll-snap-type: y mandatory;\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Sidebar container\n.md-sidebar {\n position: sticky;\n top: px2rem(48px);\n align-self: flex-start;\n width: px2rem(242px);\n padding: px2rem(24px) 0;\n overflow: hidden;\n\n // Hide for print\n @media print {\n display: none;\n }\n\n // [tablet -]: Convert navigation to drawer\n @include break-to-device(tablet) {\n\n // Render primary sidebar as a slideout container\n &--primary {\n position: fixed;\n top: 0;\n left: px2rem(-242px);\n z-index: 3;\n width: px2rem(242px);\n height: 100%;\n background-color: var(--md-default-bg-color);\n transform: translateX(0);\n transition:\n transform 250ms cubic-bezier(0.4, 0, 0.2, 1),\n box-shadow 250ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2rem(-242px);\n left: initial;\n }\n\n // Expanded drawer\n #{$md-toggle__drawer--checked} & {\n @include z-depth(8);\n\n transform: translateX(px2rem(242px));\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n transform: translateX(px2rem(-242px));\n }\n }\n\n // Hide overflow for nested navigation\n .md-sidebar__scrollwrap {\n overflow: hidden;\n }\n }\n }\n\n // Secondary sidebar with table of contents\n &--secondary {\n display: none;\n order: 2;\n\n // [tablet landscape +]: Show table of contents next to body copy\n @include break-from-device(tablet landscape) {\n display: block;\n\n // Ensure smooth scrolling on iOS\n .md-sidebar__scrollwrap {\n touch-action: pan-y;\n }\n }\n }\n\n // Wrapper for scrolling on overflow\n &__scrollwrap {\n max-height: 100%;\n margin: 0 px2rem(4px);\n overflow-y: auto;\n // Hack: reduce jitter\n backface-visibility: hidden;\n // Override Firefox scrollbar style\n scrollbar-width: thin;\n scrollbar-color: var(--md-default-fg-color--lighter) transparent;\n\n // Hack: Chrome 81+ exhibits a strange bug, where it scrolls the container\n // to the bottom if `scroll-snap-type` is set on the initial render. For\n // this reason, we use an animation to set scroll snaping with a slight\n // delay, which seems to fix the issue (#1667).\n .js & {\n animation: md-sidebar__scrollwrap--hack 400ms forwards;\n }\n\n // [tablet -]: Adjust margins\n @include break-to-device(tablet) {\n\n // Stretch scrollwrap for primary sidebar\n .md-sidebar--primary & {\n position: absolute;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n margin: 0;\n scroll-snap-type: none;\n }\n }\n\n // Override Firefox scrollbar hover color\n &:hover {\n scrollbar-color: var(--md-accent-fg-color) transparent;\n }\n\n // Override native scrollbar styles\n &::-webkit-scrollbar {\n width: px2rem(4px);\n height: px2rem(4px);\n }\n\n // Scrollbar thumb\n &::-webkit-scrollbar-thumb {\n background-color: var(--md-default-fg-color--lighter);\n\n // Hovered scrollbar thumb\n &:hover {\n background-color: var(--md-accent-fg-color);\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Keyframes\n// ----------------------------------------------------------------------------\n\n// Show source facts\n@keyframes md-source__facts--done {\n 0% {\n height: 0;\n }\n\n 100% {\n height: px2rem(13px);\n }\n}\n\n// Show source fact\n@keyframes md-source__fact--done {\n 0% {\n transform: translateY(100%);\n opacity: 0;\n }\n\n 50% {\n opacity: 0;\n }\n\n 100% {\n transform: translateY(0%);\n opacity: 1;\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Source container\n.md-source {\n display: block;\n font-size: px2rem(13px);\n line-height: 1.2;\n white-space: nowrap;\n // Hack: reduce jitter\n backface-visibility: hidden;\n transition: opacity 250ms;\n\n // Hovered source container\n &:hover {\n opacity: 0.7;\n }\n\n // Repository platform icon\n &__icon {\n display: inline-block;\n width: px2rem(48px);\n height: px2rem(48px);\n vertical-align: middle;\n\n // Align with margin only (as opposed to normal button alignment)\n svg {\n margin-top: px2rem(12px);\n margin-left: px2rem(12px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2rem(12px);\n margin-left: initial;\n }\n }\n\n // Correct alignment, if icon is present\n + .md-source__repository {\n margin-left: px2rem(-40px);\n padding-left: px2rem(40px);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2rem(-40px);\n margin-left: initial;\n padding-right: px2rem(40px);\n padding-left: initial;\n }\n }\n }\n\n // Repository name\n &__repository {\n display: inline-block;\n max-width: calc(100% - #{px2rem(24px)});\n margin-left: px2rem(12px);\n overflow: hidden;\n font-weight: 700;\n text-overflow: ellipsis;\n vertical-align: middle;\n }\n\n // Source facts (statistics etc.)\n &__facts {\n margin: 0;\n padding: 0;\n overflow: hidden;\n font-weight: 700;\n font-size: px2rem(11px);\n list-style-type: none;\n opacity: 0.75;\n\n // Show after the data was loaded\n [data-md-state=\"done\"] & {\n animation: md-source__facts--done 250ms ease-in;\n }\n }\n\n // Fact\n &__fact {\n float: left;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n float: right;\n }\n\n // Show after the data was loaded\n [data-md-state=\"done\"] & {\n animation: md-source__fact--done 400ms ease-out;\n }\n\n // Middle dot before fact\n &::before {\n margin: 0 px2rem(2px);\n content: \"\\00B7\";\n }\n\n // Remove middle dot on first fact\n &:first-child::before {\n display: none;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Tabs with outline\n.md-tabs {\n width: 100%;\n overflow: auto;\n color: var(--md-primary-bg-color);\n background-color: var(--md-primary-fg-color);\n transition: background 250ms;\n\n // Omit transitions, in case JavaScript is not available\n .no-js & {\n transition: none;\n }\n\n // [tablet -]: Hide tabs for tablet and below, as they don't make any sense\n @include break-to-device(tablet) {\n display: none;\n }\n\n // Hide for print\n @media print {\n display: none;\n }\n\n // List of items\n &__list {\n margin: 0;\n margin-left: px2rem(4px);\n padding: 0;\n white-space: nowrap;\n list-style: none;\n contain: content;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin-right: px2rem(4px);\n margin-left: initial;\n }\n }\n\n // List item\n &__item {\n display: inline-block;\n height: px2rem(48px);\n padding-right: px2rem(12px);\n padding-left: px2rem(12px);\n }\n\n // Link inside item - could be defined as block elements and aligned via\n // line height, but this would imply more repaints when scrolling\n &__link {\n display: block;\n margin-top: px2rem(16px);\n font-size: px2rem(14px);\n opacity: 0.7;\n transition:\n transform 400ms cubic-bezier(0.1, 0.7, 0.1, 1),\n opacity 250ms;\n\n // Omit transitions, in case JavaScript is not available\n .no-js & {\n transition: none;\n }\n\n // Active or hovered link\n &--active,\n &:hover {\n color: inherit;\n opacity: 1;\n }\n\n // Delay transitions by a small amount\n @for $i from 2 through 16 {\n .md-tabs__item:nth-child(#{$i}) & {\n transition-delay: 20ms * ($i - 1);\n }\n }\n }\n\n // Fade-out tabs background upon scrolling\n &[data-md-state=\"hidden\"] {\n pointer-events: none;\n\n // Hide tabs upon scrolling - disable transition to minimizes repaints\n // while scrolling down, while scrolling up seems to be okay\n .md-tabs__link {\n transform: translateY(50%);\n opacity: 0;\n transition:\n color 250ms,\n transform 0ms 400ms,\n opacity 100ms;\n }\n }\n\n // [screen +]: Adjust main navigation styles\n @include break-from-device(screen) {\n\n // Hide 1st level nested items, as they are listed in the tabs\n ~ .md-main .md-nav--primary > .md-nav__list > .md-nav__item--nested {\n display: none;\n }\n\n // Active tab\n &--active ~ .md-main {\n\n // Adjust 1st level styles\n .md-nav--primary {\n\n // Show title and remove spacing\n .md-nav__title {\n display: block;\n padding: 0 px2rem(12px);\n pointer-events: none;\n scroll-snap-align: start;\n\n // Hide site title\n &[for=\"__drawer\"] {\n display: none;\n }\n }\n\n // Hide 1st level items\n > .md-nav__list > .md-nav__item {\n display: none;\n\n // Show 1st level active nested items\n &--active {\n display: block;\n padding: 0;\n\n // Hide nested links\n > .md-nav__link {\n display: none;\n }\n }\n }\n }\n\n // Always expand nested navigation on 2nd level\n .md-nav[data-md-level=\"1\"] {\n // Hack: Always show active navigation tab on breakpoint screen, despite\n // of checkbox being checked or not. Fixes #1655.\n display: block;\n\n // Remove spacing on 2nd level items\n > .md-nav__list > .md-nav__item {\n padding: 0 px2rem(12px);\n\n // Add bottom spacing to last item\n &:last-child {\n padding-bottom: px2rem(12px);\n\n // Remove bottom spacing for nested items\n .md-nav__item {\n padding-bottom: 0;\n }\n }\n }\n\n // Hide titles from 2nd level on\n .md-nav .md-nav__title {\n display: none;\n }\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Variables\n// ----------------------------------------------------------------------------\n\n///\n/// Admonition flavours\n///\n$admonitions: (\n note: pencil $clr-blue-a200,\n abstract summary tldr: text-subject $clr-light-blue-a400,\n info todo: information $clr-cyan-a700,\n tip hint important: fire $clr-teal-a700,\n success check done: check-circle $clr-green-a700,\n question help faq: help-circle $clr-light-green-a700,\n warning caution attention: alert $clr-orange-a400,\n failure fail missing: close-circle $clr-red-a200,\n danger error: flash-circle $clr-red-a400,\n bug: bug $clr-pink-a400,\n example: format-list-numbered $clr-deep-purple-a400,\n quote cite: format-quote-close $clr-grey\n) !default;\n\n// ----------------------------------------------------------------------------\n// Rules: layout\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n @each $names, $props in $admonitions {\n $name: nth($names, 1);\n $icon: nth($props, 1);\n\n // Inline icon through PostCSS in Webpack\n --md-admonition-icon--#{$name}: svg-load(\"@mdi/svg/svg/#{$icon}.svg\");\n }\n}\n\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Admonition extension\n .admonition {\n margin: px2em(20px, 12.8px) 0;\n padding: 0 px2rem(12px);\n overflow: hidden;\n color: var(--md-admonition-fg-color);\n font-size: px2rem(12.8px);\n page-break-inside: avoid;\n background-color: var(--md-admonition-bg-color);\n border-left: px2rem(4px) solid $clr-blue-a200;\n border-radius: px2rem(2px);\n box-shadow:\n 0 px2rem(4px) px2rem(10px) hsla(0, 0%, 0%, 0.05),\n 0 0 px2rem(1px) hsla(0, 0%, 0%, 0.1);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n border-right: px2rem(4px) solid $clr-blue-a200;\n border-left: none;\n }\n\n // Hack: omit rendering errors for print\n @media print {\n box-shadow: none;\n }\n\n // Adjust spacing on last element\n html & > :last-child {\n margin-bottom: px2rem(12px);\n }\n\n // Adjust margin for nested admonition blocks\n .admonition {\n margin: 1em 0;\n }\n\n // Wrapper for scrolling on overflow\n .md-typeset__scrollwrap {\n margin: 1em px2rem(-12px);\n }\n\n // Data table wrapper, in case JavaScript is available\n .md-typeset__table {\n padding: 0 px2rem(12px);\n }\n\n // Tabbed block container is the only child\n > .tabbed-set:only-child {\n margin-top: 0;\n }\n }\n\n // Admonition title\n .admonition-title {\n position: relative;\n margin: 0 px2rem(-12px) 0 px2rem(-16px);\n padding: px2rem(8px) px2rem(12px) px2rem(8px) px2rem(44px);\n font-weight: 700;\n background-color: transparentize($clr-blue-a200, 0.9);\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n margin: 0 px2rem(-16px) 0 px2rem(-12px);\n padding: px2rem(8px) px2rem(40px) px2rem(8px) px2rem(12px);\n }\n\n // Reset spacing, if title is the only element\n html &:last-child {\n margin-bottom: 0;\n }\n\n // Admonition icon\n &::before {\n position: absolute;\n left: px2rem(16px);\n width: px2rem(20px);\n height: px2rem(20px);\n background-color: $clr-blue-a200;\n mask-image: var(--md-admonition-icon--note);\n mask-repeat: no-repeat;\n content: \"\";\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2rem(16px);\n left: initial;\n }\n }\n\n // Reset code inside admonition titles\n code {\n margin: initial;\n padding: initial;\n color: currentColor;\n background-color: transparent;\n border-radius: initial;\n box-shadow: none;\n }\n\n // Tabbed block container is the last child\n + .tabbed-set:last-child {\n margin-top: 0;\n }\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules: flavours\n// ----------------------------------------------------------------------------\n\n@each $names, $props in $admonitions {\n $name: nth($names, 1);\n $tint: nth($props, 2);\n\n // Admonition base class\n .md-typeset .admonition.#{$name} {\n border-color: $tint;\n }\n\n // Admonition title\n .md-typeset .#{$name} > .admonition-title {\n background-color: transparentize($tint, 0.9);\n\n // Admonition icon\n &::before {\n background-color: $tint;\n mask-image: var(--md-admonition-icon--#{$name});\n mask-repeat: no-repeat;\n }\n }\n\n // Define synonyms for base class\n @if length($names) > 1 {\n @for $n from 2 through length($names) {\n .#{nth($names, $n)} {\n @extend .#{$name};\n }\n }\n }\n}\n","// ==========================================================================\n//\n// Name: UI Color Palette\n// Description: The color palette of material design.\n// Version: 2.3.1\n//\n// Author: Denis Malinochkin\n// Git: https://github.com/mrmlnc/material-color\n//\n// twitter: @mrmlnc\n//\n// ==========================================================================\n\n\n//\n// List of base colors\n//\n\n// $clr-red\n// $clr-pink\n// $clr-purple\n// $clr-deep-purple\n// $clr-indigo\n// $clr-blue\n// $clr-light-blue\n// $clr-cyan\n// $clr-teal\n// $clr-green\n// $clr-light-green\n// $clr-lime\n// $clr-yellow\n// $clr-amber\n// $clr-orange\n// $clr-deep-orange\n// $clr-brown\n// $clr-grey\n// $clr-blue-grey\n// $clr-black\n// $clr-white\n\n\n//\n// Red\n//\n\n$clr-red-list: (\n \"base\": #f44336,\n \"50\": #ffebee,\n \"100\": #ffcdd2,\n \"200\": #ef9a9a,\n \"300\": #e57373,\n \"400\": #ef5350,\n \"500\": #f44336,\n \"600\": #e53935,\n \"700\": #d32f2f,\n \"800\": #c62828,\n \"900\": #b71c1c,\n \"a100\": #ff8a80,\n \"a200\": #ff5252,\n \"a400\": #ff1744,\n \"a700\": #d50000\n);\n\n$clr-red: map-get($clr-red-list, \"base\");\n\n$clr-red-50: map-get($clr-red-list, \"50\");\n$clr-red-100: map-get($clr-red-list, \"100\");\n$clr-red-200: map-get($clr-red-list, \"200\");\n$clr-red-300: map-get($clr-red-list, \"300\");\n$clr-red-400: map-get($clr-red-list, \"400\");\n$clr-red-500: map-get($clr-red-list, \"500\");\n$clr-red-600: map-get($clr-red-list, \"600\");\n$clr-red-700: map-get($clr-red-list, \"700\");\n$clr-red-800: map-get($clr-red-list, \"800\");\n$clr-red-900: map-get($clr-red-list, \"900\");\n$clr-red-a100: map-get($clr-red-list, \"a100\");\n$clr-red-a200: map-get($clr-red-list, \"a200\");\n$clr-red-a400: map-get($clr-red-list, \"a400\");\n$clr-red-a700: map-get($clr-red-list, \"a700\");\n\n\n//\n// Pink\n//\n\n$clr-pink-list: (\n \"base\": #e91e63,\n \"50\": #fce4ec,\n \"100\": #f8bbd0,\n \"200\": #f48fb1,\n \"300\": #f06292,\n \"400\": #ec407a,\n \"500\": #e91e63,\n \"600\": #d81b60,\n \"700\": #c2185b,\n \"800\": #ad1457,\n \"900\": #880e4f,\n \"a100\": #ff80ab,\n \"a200\": #ff4081,\n \"a400\": #f50057,\n \"a700\": #c51162\n);\n\n$clr-pink: map-get($clr-pink-list, \"base\");\n\n$clr-pink-50: map-get($clr-pink-list, \"50\");\n$clr-pink-100: map-get($clr-pink-list, \"100\");\n$clr-pink-200: map-get($clr-pink-list, \"200\");\n$clr-pink-300: map-get($clr-pink-list, \"300\");\n$clr-pink-400: map-get($clr-pink-list, \"400\");\n$clr-pink-500: map-get($clr-pink-list, \"500\");\n$clr-pink-600: map-get($clr-pink-list, \"600\");\n$clr-pink-700: map-get($clr-pink-list, \"700\");\n$clr-pink-800: map-get($clr-pink-list, \"800\");\n$clr-pink-900: map-get($clr-pink-list, \"900\");\n$clr-pink-a100: map-get($clr-pink-list, \"a100\");\n$clr-pink-a200: map-get($clr-pink-list, \"a200\");\n$clr-pink-a400: map-get($clr-pink-list, \"a400\");\n$clr-pink-a700: map-get($clr-pink-list, \"a700\");\n\n\n//\n// Purple\n//\n\n$clr-purple-list: (\n \"base\": #9c27b0,\n \"50\": #f3e5f5,\n \"100\": #e1bee7,\n \"200\": #ce93d8,\n \"300\": #ba68c8,\n \"400\": #ab47bc,\n \"500\": #9c27b0,\n \"600\": #8e24aa,\n \"700\": #7b1fa2,\n \"800\": #6a1b9a,\n \"900\": #4a148c,\n \"a100\": #ea80fc,\n \"a200\": #e040fb,\n \"a400\": #d500f9,\n \"a700\": #aa00ff\n);\n\n$clr-purple: map-get($clr-purple-list, \"base\");\n\n$clr-purple-50: map-get($clr-purple-list, \"50\");\n$clr-purple-100: map-get($clr-purple-list, \"100\");\n$clr-purple-200: map-get($clr-purple-list, \"200\");\n$clr-purple-300: map-get($clr-purple-list, \"300\");\n$clr-purple-400: map-get($clr-purple-list, \"400\");\n$clr-purple-500: map-get($clr-purple-list, \"500\");\n$clr-purple-600: map-get($clr-purple-list, \"600\");\n$clr-purple-700: map-get($clr-purple-list, \"700\");\n$clr-purple-800: map-get($clr-purple-list, \"800\");\n$clr-purple-900: map-get($clr-purple-list, \"900\");\n$clr-purple-a100: map-get($clr-purple-list, \"a100\");\n$clr-purple-a200: map-get($clr-purple-list, \"a200\");\n$clr-purple-a400: map-get($clr-purple-list, \"a400\");\n$clr-purple-a700: map-get($clr-purple-list, \"a700\");\n\n\n//\n// Deep purple\n//\n\n$clr-deep-purple-list: (\n \"base\": #673ab7,\n \"50\": #ede7f6,\n \"100\": #d1c4e9,\n \"200\": #b39ddb,\n \"300\": #9575cd,\n \"400\": #7e57c2,\n \"500\": #673ab7,\n \"600\": #5e35b1,\n \"700\": #512da8,\n \"800\": #4527a0,\n \"900\": #311b92,\n \"a100\": #b388ff,\n \"a200\": #7c4dff,\n \"a400\": #651fff,\n \"a700\": #6200ea\n);\n\n$clr-deep-purple: map-get($clr-deep-purple-list, \"base\");\n\n$clr-deep-purple-50: map-get($clr-deep-purple-list, \"50\");\n$clr-deep-purple-100: map-get($clr-deep-purple-list, \"100\");\n$clr-deep-purple-200: map-get($clr-deep-purple-list, \"200\");\n$clr-deep-purple-300: map-get($clr-deep-purple-list, \"300\");\n$clr-deep-purple-400: map-get($clr-deep-purple-list, \"400\");\n$clr-deep-purple-500: map-get($clr-deep-purple-list, \"500\");\n$clr-deep-purple-600: map-get($clr-deep-purple-list, \"600\");\n$clr-deep-purple-700: map-get($clr-deep-purple-list, \"700\");\n$clr-deep-purple-800: map-get($clr-deep-purple-list, \"800\");\n$clr-deep-purple-900: map-get($clr-deep-purple-list, \"900\");\n$clr-deep-purple-a100: map-get($clr-deep-purple-list, \"a100\");\n$clr-deep-purple-a200: map-get($clr-deep-purple-list, \"a200\");\n$clr-deep-purple-a400: map-get($clr-deep-purple-list, \"a400\");\n$clr-deep-purple-a700: map-get($clr-deep-purple-list, \"a700\");\n\n\n//\n// Indigo\n//\n\n$clr-indigo-list: (\n \"base\": #3f51b5,\n \"50\": #e8eaf6,\n \"100\": #c5cae9,\n \"200\": #9fa8da,\n \"300\": #7986cb,\n \"400\": #5c6bc0,\n \"500\": #3f51b5,\n \"600\": #3949ab,\n \"700\": #303f9f,\n \"800\": #283593,\n \"900\": #1a237e,\n \"a100\": #8c9eff,\n \"a200\": #536dfe,\n \"a400\": #3d5afe,\n \"a700\": #304ffe\n);\n\n$clr-indigo: map-get($clr-indigo-list, \"base\");\n\n$clr-indigo-50: map-get($clr-indigo-list, \"50\");\n$clr-indigo-100: map-get($clr-indigo-list, \"100\");\n$clr-indigo-200: map-get($clr-indigo-list, \"200\");\n$clr-indigo-300: map-get($clr-indigo-list, \"300\");\n$clr-indigo-400: map-get($clr-indigo-list, \"400\");\n$clr-indigo-500: map-get($clr-indigo-list, \"500\");\n$clr-indigo-600: map-get($clr-indigo-list, \"600\");\n$clr-indigo-700: map-get($clr-indigo-list, \"700\");\n$clr-indigo-800: map-get($clr-indigo-list, \"800\");\n$clr-indigo-900: map-get($clr-indigo-list, \"900\");\n$clr-indigo-a100: map-get($clr-indigo-list, \"a100\");\n$clr-indigo-a200: map-get($clr-indigo-list, \"a200\");\n$clr-indigo-a400: map-get($clr-indigo-list, \"a400\");\n$clr-indigo-a700: map-get($clr-indigo-list, \"a700\");\n\n\n//\n// Blue\n//\n\n$clr-blue-list: (\n \"base\": #2196f3,\n \"50\": #e3f2fd,\n \"100\": #bbdefb,\n \"200\": #90caf9,\n \"300\": #64b5f6,\n \"400\": #42a5f5,\n \"500\": #2196f3,\n \"600\": #1e88e5,\n \"700\": #1976d2,\n \"800\": #1565c0,\n \"900\": #0d47a1,\n \"a100\": #82b1ff,\n \"a200\": #448aff,\n \"a400\": #2979ff,\n \"a700\": #2962ff\n);\n\n$clr-blue: map-get($clr-blue-list, \"base\");\n\n$clr-blue-50: map-get($clr-blue-list, \"50\");\n$clr-blue-100: map-get($clr-blue-list, \"100\");\n$clr-blue-200: map-get($clr-blue-list, \"200\");\n$clr-blue-300: map-get($clr-blue-list, \"300\");\n$clr-blue-400: map-get($clr-blue-list, \"400\");\n$clr-blue-500: map-get($clr-blue-list, \"500\");\n$clr-blue-600: map-get($clr-blue-list, \"600\");\n$clr-blue-700: map-get($clr-blue-list, \"700\");\n$clr-blue-800: map-get($clr-blue-list, \"800\");\n$clr-blue-900: map-get($clr-blue-list, \"900\");\n$clr-blue-a100: map-get($clr-blue-list, \"a100\");\n$clr-blue-a200: map-get($clr-blue-list, \"a200\");\n$clr-blue-a400: map-get($clr-blue-list, \"a400\");\n$clr-blue-a700: map-get($clr-blue-list, \"a700\");\n\n\n//\n// Light Blue\n//\n\n$clr-light-blue-list: (\n \"base\": #03a9f4,\n \"50\": #e1f5fe,\n \"100\": #b3e5fc,\n \"200\": #81d4fa,\n \"300\": #4fc3f7,\n \"400\": #29b6f6,\n \"500\": #03a9f4,\n \"600\": #039be5,\n \"700\": #0288d1,\n \"800\": #0277bd,\n \"900\": #01579b,\n \"a100\": #80d8ff,\n \"a200\": #40c4ff,\n \"a400\": #00b0ff,\n \"a700\": #0091ea\n);\n\n$clr-light-blue: map-get($clr-light-blue-list, \"base\");\n\n$clr-light-blue-50: map-get($clr-light-blue-list, \"50\");\n$clr-light-blue-100: map-get($clr-light-blue-list, \"100\");\n$clr-light-blue-200: map-get($clr-light-blue-list, \"200\");\n$clr-light-blue-300: map-get($clr-light-blue-list, \"300\");\n$clr-light-blue-400: map-get($clr-light-blue-list, \"400\");\n$clr-light-blue-500: map-get($clr-light-blue-list, \"500\");\n$clr-light-blue-600: map-get($clr-light-blue-list, \"600\");\n$clr-light-blue-700: map-get($clr-light-blue-list, \"700\");\n$clr-light-blue-800: map-get($clr-light-blue-list, \"800\");\n$clr-light-blue-900: map-get($clr-light-blue-list, \"900\");\n$clr-light-blue-a100: map-get($clr-light-blue-list, \"a100\");\n$clr-light-blue-a200: map-get($clr-light-blue-list, \"a200\");\n$clr-light-blue-a400: map-get($clr-light-blue-list, \"a400\");\n$clr-light-blue-a700: map-get($clr-light-blue-list, \"a700\");\n\n\n//\n// Cyan\n//\n\n$clr-cyan-list: (\n \"base\": #00bcd4,\n \"50\": #e0f7fa,\n \"100\": #b2ebf2,\n \"200\": #80deea,\n \"300\": #4dd0e1,\n \"400\": #26c6da,\n \"500\": #00bcd4,\n \"600\": #00acc1,\n \"700\": #0097a7,\n \"800\": #00838f,\n \"900\": #006064,\n \"a100\": #84ffff,\n \"a200\": #18ffff,\n \"a400\": #00e5ff,\n \"a700\": #00b8d4\n);\n\n$clr-cyan: map-get($clr-cyan-list, \"base\");\n\n$clr-cyan-50: map-get($clr-cyan-list, \"50\");\n$clr-cyan-100: map-get($clr-cyan-list, \"100\");\n$clr-cyan-200: map-get($clr-cyan-list, \"200\");\n$clr-cyan-300: map-get($clr-cyan-list, \"300\");\n$clr-cyan-400: map-get($clr-cyan-list, \"400\");\n$clr-cyan-500: map-get($clr-cyan-list, \"500\");\n$clr-cyan-600: map-get($clr-cyan-list, \"600\");\n$clr-cyan-700: map-get($clr-cyan-list, \"700\");\n$clr-cyan-800: map-get($clr-cyan-list, \"800\");\n$clr-cyan-900: map-get($clr-cyan-list, \"900\");\n$clr-cyan-a100: map-get($clr-cyan-list, \"a100\");\n$clr-cyan-a200: map-get($clr-cyan-list, \"a200\");\n$clr-cyan-a400: map-get($clr-cyan-list, \"a400\");\n$clr-cyan-a700: map-get($clr-cyan-list, \"a700\");\n\n\n//\n// Teal\n//\n\n$clr-teal-list: (\n \"base\": #009688,\n \"50\": #e0f2f1,\n \"100\": #b2dfdb,\n \"200\": #80cbc4,\n \"300\": #4db6ac,\n \"400\": #26a69a,\n \"500\": #009688,\n \"600\": #00897b,\n \"700\": #00796b,\n \"800\": #00695c,\n \"900\": #004d40,\n \"a100\": #a7ffeb,\n \"a200\": #64ffda,\n \"a400\": #1de9b6,\n \"a700\": #00bfa5\n);\n\n$clr-teal: map-get($clr-teal-list, \"base\");\n\n$clr-teal-50: map-get($clr-teal-list, \"50\");\n$clr-teal-100: map-get($clr-teal-list, \"100\");\n$clr-teal-200: map-get($clr-teal-list, \"200\");\n$clr-teal-300: map-get($clr-teal-list, \"300\");\n$clr-teal-400: map-get($clr-teal-list, \"400\");\n$clr-teal-500: map-get($clr-teal-list, \"500\");\n$clr-teal-600: map-get($clr-teal-list, \"600\");\n$clr-teal-700: map-get($clr-teal-list, \"700\");\n$clr-teal-800: map-get($clr-teal-list, \"800\");\n$clr-teal-900: map-get($clr-teal-list, \"900\");\n$clr-teal-a100: map-get($clr-teal-list, \"a100\");\n$clr-teal-a200: map-get($clr-teal-list, \"a200\");\n$clr-teal-a400: map-get($clr-teal-list, \"a400\");\n$clr-teal-a700: map-get($clr-teal-list, \"a700\");\n\n\n//\n// Green\n//\n\n$clr-green-list: (\n \"base\": #4caf50,\n \"50\": #e8f5e9,\n \"100\": #c8e6c9,\n \"200\": #a5d6a7,\n \"300\": #81c784,\n \"400\": #66bb6a,\n \"500\": #4caf50,\n \"600\": #43a047,\n \"700\": #388e3c,\n \"800\": #2e7d32,\n \"900\": #1b5e20,\n \"a100\": #b9f6ca,\n \"a200\": #69f0ae,\n \"a400\": #00e676,\n \"a700\": #00c853\n);\n\n$clr-green: map-get($clr-green-list, \"base\");\n\n$clr-green-50: map-get($clr-green-list, \"50\");\n$clr-green-100: map-get($clr-green-list, \"100\");\n$clr-green-200: map-get($clr-green-list, \"200\");\n$clr-green-300: map-get($clr-green-list, \"300\");\n$clr-green-400: map-get($clr-green-list, \"400\");\n$clr-green-500: map-get($clr-green-list, \"500\");\n$clr-green-600: map-get($clr-green-list, \"600\");\n$clr-green-700: map-get($clr-green-list, \"700\");\n$clr-green-800: map-get($clr-green-list, \"800\");\n$clr-green-900: map-get($clr-green-list, \"900\");\n$clr-green-a100: map-get($clr-green-list, \"a100\");\n$clr-green-a200: map-get($clr-green-list, \"a200\");\n$clr-green-a400: map-get($clr-green-list, \"a400\");\n$clr-green-a700: map-get($clr-green-list, \"a700\");\n\n\n//\n// Light green\n//\n\n$clr-light-green-list: (\n \"base\": #8bc34a,\n \"50\": #f1f8e9,\n \"100\": #dcedc8,\n \"200\": #c5e1a5,\n \"300\": #aed581,\n \"400\": #9ccc65,\n \"500\": #8bc34a,\n \"600\": #7cb342,\n \"700\": #689f38,\n \"800\": #558b2f,\n \"900\": #33691e,\n \"a100\": #ccff90,\n \"a200\": #b2ff59,\n \"a400\": #76ff03,\n \"a700\": #64dd17\n);\n\n$clr-light-green: map-get($clr-light-green-list, \"base\");\n\n$clr-light-green-50: map-get($clr-light-green-list, \"50\");\n$clr-light-green-100: map-get($clr-light-green-list, \"100\");\n$clr-light-green-200: map-get($clr-light-green-list, \"200\");\n$clr-light-green-300: map-get($clr-light-green-list, \"300\");\n$clr-light-green-400: map-get($clr-light-green-list, \"400\");\n$clr-light-green-500: map-get($clr-light-green-list, \"500\");\n$clr-light-green-600: map-get($clr-light-green-list, \"600\");\n$clr-light-green-700: map-get($clr-light-green-list, \"700\");\n$clr-light-green-800: map-get($clr-light-green-list, \"800\");\n$clr-light-green-900: map-get($clr-light-green-list, \"900\");\n$clr-light-green-a100: map-get($clr-light-green-list, \"a100\");\n$clr-light-green-a200: map-get($clr-light-green-list, \"a200\");\n$clr-light-green-a400: map-get($clr-light-green-list, \"a400\");\n$clr-light-green-a700: map-get($clr-light-green-list, \"a700\");\n\n\n//\n// Lime\n//\n\n$clr-lime-list: (\n \"base\": #cddc39,\n \"50\": #f9fbe7,\n \"100\": #f0f4c3,\n \"200\": #e6ee9c,\n \"300\": #dce775,\n \"400\": #d4e157,\n \"500\": #cddc39,\n \"600\": #c0ca33,\n \"700\": #afb42b,\n \"800\": #9e9d24,\n \"900\": #827717,\n \"a100\": #f4ff81,\n \"a200\": #eeff41,\n \"a400\": #c6ff00,\n \"a700\": #aeea00\n);\n\n$clr-lime: map-get($clr-lime-list, \"base\");\n\n$clr-lime-50: map-get($clr-lime-list, \"50\");\n$clr-lime-100: map-get($clr-lime-list, \"100\");\n$clr-lime-200: map-get($clr-lime-list, \"200\");\n$clr-lime-300: map-get($clr-lime-list, \"300\");\n$clr-lime-400: map-get($clr-lime-list, \"400\");\n$clr-lime-500: map-get($clr-lime-list, \"500\");\n$clr-lime-600: map-get($clr-lime-list, \"600\");\n$clr-lime-700: map-get($clr-lime-list, \"700\");\n$clr-lime-800: map-get($clr-lime-list, \"800\");\n$clr-lime-900: map-get($clr-lime-list, \"900\");\n$clr-lime-a100: map-get($clr-lime-list, \"a100\");\n$clr-lime-a200: map-get($clr-lime-list, \"a200\");\n$clr-lime-a400: map-get($clr-lime-list, \"a400\");\n$clr-lime-a700: map-get($clr-lime-list, \"a700\");\n\n\n//\n// Yellow\n//\n\n$clr-yellow-list: (\n \"base\": #ffeb3b,\n \"50\": #fffde7,\n \"100\": #fff9c4,\n \"200\": #fff59d,\n \"300\": #fff176,\n \"400\": #ffee58,\n \"500\": #ffeb3b,\n \"600\": #fdd835,\n \"700\": #fbc02d,\n \"800\": #f9a825,\n \"900\": #f57f17,\n \"a100\": #ffff8d,\n \"a200\": #ffff00,\n \"a400\": #ffea00,\n \"a700\": #ffd600\n);\n\n$clr-yellow: map-get($clr-yellow-list, \"base\");\n\n$clr-yellow-50: map-get($clr-yellow-list, \"50\");\n$clr-yellow-100: map-get($clr-yellow-list, \"100\");\n$clr-yellow-200: map-get($clr-yellow-list, \"200\");\n$clr-yellow-300: map-get($clr-yellow-list, \"300\");\n$clr-yellow-400: map-get($clr-yellow-list, \"400\");\n$clr-yellow-500: map-get($clr-yellow-list, \"500\");\n$clr-yellow-600: map-get($clr-yellow-list, \"600\");\n$clr-yellow-700: map-get($clr-yellow-list, \"700\");\n$clr-yellow-800: map-get($clr-yellow-list, \"800\");\n$clr-yellow-900: map-get($clr-yellow-list, \"900\");\n$clr-yellow-a100: map-get($clr-yellow-list, \"a100\");\n$clr-yellow-a200: map-get($clr-yellow-list, \"a200\");\n$clr-yellow-a400: map-get($clr-yellow-list, \"a400\");\n$clr-yellow-a700: map-get($clr-yellow-list, \"a700\");\n\n\n//\n// amber\n//\n\n$clr-amber-list: (\n \"base\": #ffc107,\n \"50\": #fff8e1,\n \"100\": #ffecb3,\n \"200\": #ffe082,\n \"300\": #ffd54f,\n \"400\": #ffca28,\n \"500\": #ffc107,\n \"600\": #ffb300,\n \"700\": #ffa000,\n \"800\": #ff8f00,\n \"900\": #ff6f00,\n \"a100\": #ffe57f,\n \"a200\": #ffd740,\n \"a400\": #ffc400,\n \"a700\": #ffab00\n);\n\n$clr-amber: map-get($clr-amber-list, \"base\");\n\n$clr-amber-50: map-get($clr-amber-list, \"50\");\n$clr-amber-100: map-get($clr-amber-list, \"100\");\n$clr-amber-200: map-get($clr-amber-list, \"200\");\n$clr-amber-300: map-get($clr-amber-list, \"300\");\n$clr-amber-400: map-get($clr-amber-list, \"400\");\n$clr-amber-500: map-get($clr-amber-list, \"500\");\n$clr-amber-600: map-get($clr-amber-list, \"600\");\n$clr-amber-700: map-get($clr-amber-list, \"700\");\n$clr-amber-800: map-get($clr-amber-list, \"800\");\n$clr-amber-900: map-get($clr-amber-list, \"900\");\n$clr-amber-a100: map-get($clr-amber-list, \"a100\");\n$clr-amber-a200: map-get($clr-amber-list, \"a200\");\n$clr-amber-a400: map-get($clr-amber-list, \"a400\");\n$clr-amber-a700: map-get($clr-amber-list, \"a700\");\n\n\n//\n// Orange\n//\n\n$clr-orange-list: (\n \"base\": #ff9800,\n \"50\": #fff3e0,\n \"100\": #ffe0b2,\n \"200\": #ffcc80,\n \"300\": #ffb74d,\n \"400\": #ffa726,\n \"500\": #ff9800,\n \"600\": #fb8c00,\n \"700\": #f57c00,\n \"800\": #ef6c00,\n \"900\": #e65100,\n \"a100\": #ffd180,\n \"a200\": #ffab40,\n \"a400\": #ff9100,\n \"a700\": #ff6d00\n);\n\n$clr-orange: map-get($clr-orange-list, \"base\");\n\n$clr-orange-50: map-get($clr-orange-list, \"50\");\n$clr-orange-100: map-get($clr-orange-list, \"100\");\n$clr-orange-200: map-get($clr-orange-list, \"200\");\n$clr-orange-300: map-get($clr-orange-list, \"300\");\n$clr-orange-400: map-get($clr-orange-list, \"400\");\n$clr-orange-500: map-get($clr-orange-list, \"500\");\n$clr-orange-600: map-get($clr-orange-list, \"600\");\n$clr-orange-700: map-get($clr-orange-list, \"700\");\n$clr-orange-800: map-get($clr-orange-list, \"800\");\n$clr-orange-900: map-get($clr-orange-list, \"900\");\n$clr-orange-a100: map-get($clr-orange-list, \"a100\");\n$clr-orange-a200: map-get($clr-orange-list, \"a200\");\n$clr-orange-a400: map-get($clr-orange-list, \"a400\");\n$clr-orange-a700: map-get($clr-orange-list, \"a700\");\n\n\n//\n// Deep orange\n//\n\n$clr-deep-orange-list: (\n \"base\": #ff5722,\n \"50\": #fbe9e7,\n \"100\": #ffccbc,\n \"200\": #ffab91,\n \"300\": #ff8a65,\n \"400\": #ff7043,\n \"500\": #ff5722,\n \"600\": #f4511e,\n \"700\": #e64a19,\n \"800\": #d84315,\n \"900\": #bf360c,\n \"a100\": #ff9e80,\n \"a200\": #ff6e40,\n \"a400\": #ff3d00,\n \"a700\": #dd2c00\n);\n\n$clr-deep-orange: map-get($clr-deep-orange-list, \"base\");\n\n$clr-deep-orange-50: map-get($clr-deep-orange-list, \"50\");\n$clr-deep-orange-100: map-get($clr-deep-orange-list, \"100\");\n$clr-deep-orange-200: map-get($clr-deep-orange-list, \"200\");\n$clr-deep-orange-300: map-get($clr-deep-orange-list, \"300\");\n$clr-deep-orange-400: map-get($clr-deep-orange-list, \"400\");\n$clr-deep-orange-500: map-get($clr-deep-orange-list, \"500\");\n$clr-deep-orange-600: map-get($clr-deep-orange-list, \"600\");\n$clr-deep-orange-700: map-get($clr-deep-orange-list, \"700\");\n$clr-deep-orange-800: map-get($clr-deep-orange-list, \"800\");\n$clr-deep-orange-900: map-get($clr-deep-orange-list, \"900\");\n$clr-deep-orange-a100: map-get($clr-deep-orange-list, \"a100\");\n$clr-deep-orange-a200: map-get($clr-deep-orange-list, \"a200\");\n$clr-deep-orange-a400: map-get($clr-deep-orange-list, \"a400\");\n$clr-deep-orange-a700: map-get($clr-deep-orange-list, \"a700\");\n\n\n//\n// Brown\n//\n\n$clr-brown-list: (\n \"base\": #795548,\n \"50\": #efebe9,\n \"100\": #d7ccc8,\n \"200\": #bcaaa4,\n \"300\": #a1887f,\n \"400\": #8d6e63,\n \"500\": #795548,\n \"600\": #6d4c41,\n \"700\": #5d4037,\n \"800\": #4e342e,\n \"900\": #3e2723,\n);\n\n$clr-brown: map-get($clr-brown-list, \"base\");\n\n$clr-brown-50: map-get($clr-brown-list, \"50\");\n$clr-brown-100: map-get($clr-brown-list, \"100\");\n$clr-brown-200: map-get($clr-brown-list, \"200\");\n$clr-brown-300: map-get($clr-brown-list, \"300\");\n$clr-brown-400: map-get($clr-brown-list, \"400\");\n$clr-brown-500: map-get($clr-brown-list, \"500\");\n$clr-brown-600: map-get($clr-brown-list, \"600\");\n$clr-brown-700: map-get($clr-brown-list, \"700\");\n$clr-brown-800: map-get($clr-brown-list, \"800\");\n$clr-brown-900: map-get($clr-brown-list, \"900\");\n\n\n//\n// Grey\n//\n\n$clr-grey-list: (\n \"base\": #9e9e9e,\n \"50\": #fafafa,\n \"100\": #f5f5f5,\n \"200\": #eeeeee,\n \"300\": #e0e0e0,\n \"400\": #bdbdbd,\n \"500\": #9e9e9e,\n \"600\": #757575,\n \"700\": #616161,\n \"800\": #424242,\n \"900\": #212121,\n);\n\n$clr-grey: map-get($clr-grey-list, \"base\");\n\n$clr-grey-50: map-get($clr-grey-list, \"50\");\n$clr-grey-100: map-get($clr-grey-list, \"100\");\n$clr-grey-200: map-get($clr-grey-list, \"200\");\n$clr-grey-300: map-get($clr-grey-list, \"300\");\n$clr-grey-400: map-get($clr-grey-list, \"400\");\n$clr-grey-500: map-get($clr-grey-list, \"500\");\n$clr-grey-600: map-get($clr-grey-list, \"600\");\n$clr-grey-700: map-get($clr-grey-list, \"700\");\n$clr-grey-800: map-get($clr-grey-list, \"800\");\n$clr-grey-900: map-get($clr-grey-list, \"900\");\n\n\n//\n// Blue grey\n//\n\n$clr-blue-grey-list: (\n \"base\": #607d8b,\n \"50\": #eceff1,\n \"100\": #cfd8dc,\n \"200\": #b0bec5,\n \"300\": #90a4ae,\n \"400\": #78909c,\n \"500\": #607d8b,\n \"600\": #546e7a,\n \"700\": #455a64,\n \"800\": #37474f,\n \"900\": #263238,\n);\n\n$clr-blue-grey: map-get($clr-blue-grey-list, \"base\");\n\n$clr-blue-grey-50: map-get($clr-blue-grey-list, \"50\");\n$clr-blue-grey-100: map-get($clr-blue-grey-list, \"100\");\n$clr-blue-grey-200: map-get($clr-blue-grey-list, \"200\");\n$clr-blue-grey-300: map-get($clr-blue-grey-list, \"300\");\n$clr-blue-grey-400: map-get($clr-blue-grey-list, \"400\");\n$clr-blue-grey-500: map-get($clr-blue-grey-list, \"500\");\n$clr-blue-grey-600: map-get($clr-blue-grey-list, \"600\");\n$clr-blue-grey-700: map-get($clr-blue-grey-list, \"700\");\n$clr-blue-grey-800: map-get($clr-blue-grey-list, \"800\");\n$clr-blue-grey-900: map-get($clr-blue-grey-list, \"900\");\n\n\n//\n// Black\n//\n\n$clr-black-list: (\n \"base\": #000\n);\n\n$clr-black: map-get($clr-black-list, \"base\");\n\n\n//\n// White\n//\n\n$clr-white-list: (\n \"base\": #fff\n);\n\n$clr-white: map-get($clr-white-list, \"base\");\n\n\n//\n// List for all Colors for looping\n//\n\n$clr-list-all: (\n \"red\": $clr-red-list,\n \"pink\": $clr-pink-list,\n \"purple\": $clr-purple-list,\n \"deep-purple\": $clr-deep-purple-list,\n \"indigo\": $clr-indigo-list,\n \"blue\": $clr-blue-list,\n \"light-blue\": $clr-light-blue-list,\n \"cyan\": $clr-cyan-list,\n \"teal\": $clr-teal-list,\n \"green\": $clr-green-list,\n \"light-green\": $clr-light-green-list,\n \"lime\": $clr-lime-list,\n \"yellow\": $clr-yellow-list,\n \"amber\": $clr-amber-list,\n \"orange\": $clr-orange-list,\n \"deep-orange\": $clr-deep-orange-list,\n \"brown\": $clr-brown-list,\n \"grey\": $clr-grey-list,\n \"blue-grey\": $clr-blue-grey-list,\n \"black\": $clr-black-list,\n \"white\": $clr-white-list\n);\n\n\n//\n// Typography\n//\n\n$clr-ui-display-4: $clr-grey-600;\n$clr-ui-display-3: $clr-grey-600;\n$clr-ui-display-2: $clr-grey-600;\n$clr-ui-display-1: $clr-grey-600;\n$clr-ui-headline: $clr-grey-900;\n$clr-ui-title: $clr-grey-900;\n$clr-ui-subhead-1: $clr-grey-900;\n$clr-ui-body-2: $clr-grey-900;\n$clr-ui-body-1: $clr-grey-900;\n$clr-ui-caption: $clr-grey-600;\n$clr-ui-menu: $clr-grey-900;\n$clr-ui-button: $clr-grey-900;\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules: syntax highlighting\n// ----------------------------------------------------------------------------\n\n// Codehilite extension\n.codehilite {\n\n .o, // Operator\n .ow { // Operator, word\n color: var(--md-code-hl-operator-color);\n }\n\n .p { // Punctuation\n color: var(--md-code-hl-punctuation-color);\n }\n\n .cpf, // Comment, preprocessor file\n .l, // Literal\n .s, // Literal, string\n .sb, // Literal, string backticks\n .sc, // Literal, string char\n .s2, // Literal, string double\n .si, // Literal, string interpol\n .s1, // Literal, string single\n .ss { // Literal, string symbol\n color: var(--md-code-hl-string-color);\n }\n\n .cp, // Comment, pre-processor\n .se, // Literal, string escape\n .sh, // Literal, string heredoc\n .sr, // Literal, string regex\n .sx { // Literal, string other\n color: var(--md-code-hl-special-color);\n }\n\n .m, // Number\n .mf, // Number, float\n .mh, // Number, hex\n .mi, // Number, integer\n .il, // Number, integer long\n .mo { // Number, octal\n color: var(--md-code-hl-number-color);\n }\n\n .k, // Keyword,\n .kd, // Keyword, declaration\n .kn, // Keyword, namespace\n .kp, // Keyword, pseudo\n .kr, // Keyword, reserved\n .kt { // Keyword, type\n color: var(--md-code-hl-keyword-color);\n }\n\n .kc, // Keyword, constant\n .n { // Name\n color: var(--md-code-hl-name-color);\n }\n\n .no, // Name, constant\n .nb, // Name, builtin\n .bp { // Name, builtin pseudo\n color: var(--md-code-hl-constant-color);\n }\n\n .nc, // Name, class\n .ne, // Name, exception\n .nf, // Name, function\n .nn { // Name, namespace\n color: var(--md-code-hl-function-color);\n }\n\n .nd, // Name, decorator\n .ni, // Name, entity\n .nl, // Name, label\n .nt { // Name, tag\n color: var(--md-code-hl-keyword-color);\n }\n\n .c, // Comment\n .cm, // Comment, multiline\n .c1, // Comment, single\n .ch, // Comment, shebang\n .cs, // Comment, special\n .sd { // Literal, string doc\n color: var(--md-code-hl-comment-color);\n }\n\n .na, // Name, attribute\n .nv, // Variable,\n .vc, // Variable, class\n .vg, // Variable, global\n .vi { // Variable, instance\n color: var(--md-code-hl-variable-color);\n }\n\n .ge, // Generic, emph\n .gr, // Generic, error\n .gh, // Generic, heading\n .go, // Generic, output\n .gp, // Generic, prompt\n .gs, // Generic, strong\n .gu, // Generic, subheading\n .gt { // Generic, traceback\n color: var(--md-code-hl-generic-color);\n }\n\n .gd, // Diff, delete\n .gi { // Diff, insert\n margin: 0 px2em(-2px);\n padding: 0 px2em(2px);\n border-radius: px2rem(2px);\n }\n\n .gd { // Diff, delete\n background-color: var(--md-typeset-del-color);\n }\n\n .gi { // Diff, insert\n background-color: var(--md-typeset-ins-color)\n }\n\n // Highlighted lines\n .hll {\n display: block;\n margin: 0 px2em(-16px, 13.6px);\n padding: 0 px2em(16px, 13.6px);\n background-color: var(--md-code-hl-color)\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules: layout\n// ----------------------------------------------------------------------------\n\n// Block with line numbers\n.codehilitetable {\n display: block;\n overflow: hidden;\n\n // Set table elements to block layout, because otherwise the whole flexbox\n // hacking won't work correctly\n tbody,\n td {\n display: block;\n padding: 0;\n }\n\n // We need to use flexbox layout, because otherwise it's not possible to\n // make the code container scroll while keeping the line numbers static\n tr {\n display: flex;\n }\n\n // The pre tags are nested inside a table, so we need to remove the\n // margin because it collapses below all the overflows\n pre {\n margin: 0;\n }\n\n // Disable user selection, so code can be easily copied without\n // accidentally also copying the line numbers\n .linenos {\n padding: px2rem(10.5px) px2em(16px, 13.6px);\n padding-right: 0;\n font-size: px2em(13.6px);\n background-color: var(--md-code-bg-color);\n user-select: none;\n }\n\n // Add spacing to line number container\n .linenodiv {\n padding-right: px2em(8px, 13.6px);\n box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lighter) inset;\n\n // Reset spacings\n pre {\n color: var(--md-default-fg-color--light);\n text-align: right;\n }\n }\n\n // The table cell containing the code container wrapper and code should\n // stretch horizontally to the remaining space\n .code {\n flex: 1;\n overflow: hidden;\n }\n}\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Block with line numbers\n .codehilitetable {\n margin: 1em 0;\n direction: ltr;\n border-radius: px2rem(2px);\n\n // Remove rounded borders\n code {\n border-radius: 0;\n }\n }\n\n // [mobile -]: Stretch to whole width\n @include break-to-device(mobile) {\n\n // Full-width container\n > .codehilite {\n margin: 1em px2rem(-16px);\n\n // Stretch highlighted lines\n .hll {\n margin: 0 px2rem(-16px);\n padding: 0 px2rem(16px);\n }\n\n // Remove rounded borders\n code {\n border-radius: 0;\n }\n }\n\n // Full-width container on top-level\n > .codehilitetable {\n margin: 1em px2rem(-16px);\n border-radius: 0;\n\n // Stretch highlighted lines\n .hll {\n margin: 0 px2rem(-16px);\n padding: 0 px2rem(16px);\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-footnotes-icon: svg-load(\"@mdi/svg/svg/keyboard-return.svg\");\n}\n\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // All footnote references\n [id^=\"fnref:\"] {\n display: inline-block;\n\n // Targeted anchor\n &:target {\n margin-top: -1 * px2rem(48px + 12px + 16px);\n padding-top: px2rem(48px + 12px + 16px);\n pointer-events: none;\n scroll-margin-top: initial;\n }\n }\n\n // All footnote back references\n [id^=\"fn:\"] {\n\n // Add spacing to anchor for offset\n &::before {\n display: none;\n height: 0;\n content: \"\";\n }\n\n // Reset, as we use the anchor-correction hack here.\n &:target {\n scroll-margin-top: initial;\n }\n\n // Targeted anchor\n &:target::before {\n display: block;\n margin-top: -1 * px2rem(48px + 12px + 10px);\n padding-top: px2rem(48px + 12px + 10px);\n pointer-events: none;\n }\n }\n\n // Footnotes extension\n .footnote {\n color: var(--md-default-fg-color--light);\n font-size: px2rem(12.8px);\n\n // Remove additional spacing on footnotes\n ol {\n margin-left: 0;\n }\n\n // Footnote\n li {\n transition: color 125ms;\n\n // Darken color for targeted footnote\n &:target {\n color: var(--md-default-fg-color);\n }\n\n // Remove spacing on first element\n :first-child {\n margin-top: 0;\n }\n\n // Make back references visible on container hover\n &:hover .footnote-backref,\n &:target .footnote-backref {\n transform: translateX(0);\n opacity: 1;\n }\n\n // Hovered back reference\n &:hover .footnote-backref:hover {\n color: var(--md-accent-fg-color);\n }\n }\n }\n\n // Footnote reference\n .footnote-ref {\n display: inline-block;\n pointer-events: initial;\n }\n\n // Footnote back reference\n .footnote-backref {\n display: inline-block;\n color: var(--md-typeset-a-color);\n // Hack: remove Unicode arrow for icon\n font-size: 0;\n vertical-align: text-bottom;\n transform: translateX(px2rem(5px));\n opacity: 0;\n transition:\n color 250ms,\n transform 250ms 250ms,\n opacity 125ms 250ms;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n transform: translateX(px2rem(-5px));\n }\n\n // Back reference icon\n &::before {\n display: inline-block;\n width: px2rem(16px);\n height: px2rem(16px);\n background-color: currentColor;\n mask-image: var(--md-footnotes-icon);\n mask-repeat: no-repeat;\n content: \"\";\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n\n // Flip icon vertically\n svg {\n transform: scaleX(-1)\n }\n }\n }\n\n // Always show for print\n @media print {\n color: var(--md-typeset-a-color);\n transform: translateX(0);\n opacity: 1;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Permalinks extension\n .headerlink {\n display: inline-block;\n margin-left: px2rem(10px);\n // Hack: if we don't set visibility hidden, the text content of the node\n // will include the headerlink character, which is why Google indexes them.\n visibility: hidden;\n opacity: 0;\n transition:\n color 250ms,\n visibility 0ms 500ms,\n opacity 125ms;\n\n // Adjust for RTL languages\n [dir=\"rtl\"] & {\n margin-right: px2rem(10px);\n margin-left: initial;\n }\n\n // Higher specificity for color due to palettes integration\n html body & {\n color: var(--md-default-fg-color--lighter);\n }\n\n // Hide for print\n @media print {\n display: none;\n }\n }\n\n // Make permalink visible on hover\n :hover > .headerlink,\n :target > .headerlink,\n .headerlink:focus {\n visibility: visible;\n opacity: 1;\n transition:\n color 250ms,\n visibility 0ms,\n opacity 125ms;\n }\n\n // Active or targeted permalink\n :target > .headerlink,\n .headerlink:focus,\n .headerlink:hover {\n color: var(--md-accent-fg-color);\n }\n\n // General scroll margin offset for anything that can be targeted. Browser\n // support is pretty decent by now, and if we wait until Edge 79+ has more\n // adoption, we can get rid of all anchor-correction hacks.\n :target {\n scroll-margin-top: px2rem(48px + 24px);\n }\n\n // Correct anchor offset for link blurring\n @each $level, $delta in (\n h1 h2 h3: 8px,\n h4: 9px,\n h5 h6: 12px,\n ) {\n %#{nth($level, 1)} {\n\n // Reset, as we use the anchor-correction hack here.\n &:target {\n scroll-margin-top: initial;\n }\n\n // Un-targeted anchor\n &::before {\n display: block;\n margin-top: -1 * px2rem($delta);\n padding-top: px2rem($delta);\n content: \"\";\n }\n\n // Targeted anchor (48px from header, 12px from sidebar offset)\n &:target::before {\n margin-top: -1 * px2rem(48px + 12px + $delta);\n padding-top: px2rem(48px + 12px + $delta);\n }\n }\n\n // Define levels\n @for $n from 1 through length($level) {\n #{nth($level, $n)}[id] {\n @extend %#{nth($level, 1)};\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Scroll math block on overflow\n div.arithmatex {\n overflow-x: scroll;\n\n // [mobile -]: Stretch to whole width\n @include break-to-device(mobile) {\n margin: 0 px2rem(-16px);\n }\n\n // MathJax integration\n > * {\n width: min-content;\n margin: 1em auto !important;\n padding: 0 px2rem(16px);\n overflow: auto;\n touch-action: auto;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Deletions, additions and comments\n del.critic,\n ins.critic,\n .critic.comment {\n box-decoration-break: clone;\n }\n\n // Deletion\n del.critic {\n background-color: var(--md-typeset-del-color);\n }\n\n // Addition\n ins.critic {\n background-color: var(--md-typeset-ins-color);\n }\n\n // Comment\n .critic.comment {\n color: var(--md-code-hl-comment-color);\n\n // Comment opening mark\n &::before {\n content: \"/* \";\n }\n\n // Comment closing mark\n &::after {\n content: \" */\";\n }\n }\n\n // Block\n .critic.block {\n display: block;\n margin: 1em 0;\n padding-right: px2rem(16px);\n padding-left: px2rem(16px);\n overflow: auto;\n box-shadow: none;\n\n // Decrease spacing on first element\n :first-child {\n margin-top: 0.5em;\n }\n\n // Decrease spacing on last element\n :last-child {\n margin-bottom: 0.5em;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-details-icon: svg-load(\"@mdi/svg/svg/chevron-right.svg\");\n}\n\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Details extension\n details {\n @extend .admonition;\n\n display: block;\n padding-top: 0;\n overflow: visible;\n\n // Rotate title icon\n &[open] > summary::after {\n transform: rotate(90deg);\n }\n\n // Remove bottom spacing for closed details\n &:not([open]) {\n padding-bottom: 0;\n\n // We cannot set overflow: hidden, as the outline would not be visible,\n // so we need to correct the border radius\n > summary {\n border-radius: px2rem(2px);\n }\n }\n\n // Hack: omit margin collapse\n &::after {\n display: table;\n content: \"\";\n }\n }\n\n // Details title\n summary {\n @extend .admonition-title;\n\n display: block;\n min-height: px2rem(20px);\n padding: px2rem(8px) px2rem(36px) px2rem(8px) px2rem(44px);\n border-top-left-radius: px2rem(2px);\n border-top-right-radius: px2rem(2px);\n cursor: pointer;\n\n // Disable focus indicator for pointer devices\n &:not(.focus-visible) {\n outline: none;\n -webkit-tap-highlight-color: transparent;\n }\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n padding: px2rem(8px) px2rem(44px) px2rem(8px) px2rem(36px);\n }\n\n // Remove default details marker\n &::-webkit-details-marker {\n display: none;\n }\n\n // Details marker\n &::after {\n position: absolute;\n top: px2rem(8px);\n right: px2rem(8px);\n width: px2rem(20px);\n height: px2rem(20px);\n background-color: currentColor;\n mask-image: var(--md-details-icon);\n mask-repeat: no-repeat;\n transform: rotate(0deg);\n transition: transform 250ms;\n content: \"\";\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: initial;\n left: px2rem(8px);\n transform: rotate(180deg);\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Emojis\n img.emojione,\n img.twemoji,\n img.gemoji {\n width: px2em(18px);\n max-height: 100%;\n vertical-align: -15%;\n }\n\n // Inlined SVG icons via mkdocs-material-extensions\n span.twemoji {\n display: inline-block;\n height: px2em(18px);\n vertical-align: text-top;\n\n // Icon\n svg {\n width: px2em(18px);\n max-height: 100%;\n fill: currentColor;\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// When pymdownx.superfences is enabled but codehilite is disabled,\n// pymdownx.highlight will be used. When this happens, the outer container\n// and tables get this class names by default\n.highlight {\n @extend .codehilite;\n\n // Inline line numbers\n [data-linenos]::before {\n position: sticky;\n left: px2em(-16px, 13.6px);\n float: left;\n margin-right: px2em(16px, 13.6px);\n margin-left: px2em(-16px, 13.6px);\n padding-left: px2em(16px, 13.6px);\n color: var(--md-default-fg-color--light);\n background-color: var(--md-code-bg-color);\n box-shadow: px2rem(-1px) 0 var(--md-default-fg-color--lighter) inset;\n content: attr(data-linenos);\n user-select: none;\n }\n}\n\n// Same as above, but for code blocks with line numbers enabled\n.highlighttable {\n @extend .codehilitetable;\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset .keys {\n\n // Keyboard key icon\n kbd::before,\n kbd::after {\n position: relative;\n margin: 0;\n color: inherit;\n -moz-osx-font-smoothing: initial;\n -webkit-font-smoothing: initial;\n }\n\n // Surrounding text\n span {\n padding: 0 px2em(3.2px);\n color: var(--md-default-fg-color--light);\n }\n\n // Build special keys with left icon\n @each $name, $code in (\n\n // Modifiers\n \"alt\": \"\\2387\",\n \"left-alt\": \"\\2387\",\n \"right-alt\": \"\\2387\",\n \"command\": \"\\2318\",\n \"left-command\": \"\\2318\",\n \"right-command\": \"\\2318\",\n \"control\": \"\\2303\",\n \"left-control\": \"\\2303\",\n \"right-control\": \"\\2303\",\n \"meta\": \"\\25C6\",\n \"left-meta\": \"\\25C6\",\n \"right-meta\": \"\\25C6\",\n \"option\": \"\\2325\",\n \"left-option\": \"\\2325\",\n \"right-option\": \"\\2325\",\n \"shift\": \"\\21E7\",\n \"left-shift\": \"\\21E7\",\n \"right-shift\": \"\\21E7\",\n \"super\": \"\\2756\",\n \"left-super\": \"\\2756\",\n \"right-super\": \"\\2756\",\n \"windows\": \"\\229E\",\n \"left-windows\": \"\\229E\",\n \"right-windows\": \"\\229E\",\n\n // Other keys\n \"arrow-down\": \"\\2193\",\n \"arrow-left\": \"\\2190\",\n \"arrow-right\": \"\\2192\",\n \"arrow-up\": \"\\2191\",\n \"backspace\": \"\\232B\",\n \"backtab\": \"\\21E4\",\n \"caps-lock\": \"\\21EA\",\n \"clear\": \"\\2327\",\n \"context-menu\": \"\\2630\",\n \"delete\": \"\\2326\",\n \"eject\": \"\\23CF\",\n \"end\": \"\\2913\",\n \"escape\": \"\\238B\",\n \"home\": \"\\2912\",\n \"insert\": \"\\2380\",\n \"page-down\": \"\\21DF\",\n \"page-up\": \"\\21DE\",\n \"print-screen\": \"\\2399\"\n ) {\n .key-#{$name} {\n &::before {\n padding-right: px2em(6.4px);\n content: $code;\n }\n }\n }\n\n // Build special keys with right icon\n @each $name, $code in (\n \"tab\": \"\\21E5\",\n \"num-enter\": \"\\2324\",\n \"enter\": \"\\23CE\"\n ) {\n .key-#{$name} {\n &::after {\n padding-left: px2em(6.4px);\n content: $code;\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Tabbed block content\n .tabbed-content {\n display: none;\n order: 99;\n width: 100%;\n box-shadow: 0 px2rem(-1px) var(--md-default-fg-color--lightest);\n\n // Mirror old superfences behavior, if there's only a single code block.\n > pre:only-child,\n > .codehilite:only-child pre,\n > .codehilitetable:only-child,\n > .highlight:only-child pre,\n > .highlighttable:only-child {\n margin: 0;\n\n // Remove rounded borders at the top\n > code {\n border-top-left-radius: 0;\n border-top-right-radius: 0;\n }\n }\n\n // Nested tabs\n > .tabbed-set {\n margin: 0;\n }\n }\n\n // Tabbed block container\n .tabbed-set {\n position: relative;\n display: flex;\n flex-wrap: wrap;\n margin: 1em 0;\n border-radius: px2rem(2px);\n\n // Hide radio buttons\n > input {\n position: absolute;\n width: 0;\n height: 0;\n opacity: 0;\n\n // Active tab label\n &:checked + label {\n color: var(--md-accent-fg-color);\n border-color: var(--md-accent-fg-color);\n\n // Show tabbed block content\n & + .tabbed-content {\n display: block;\n }\n }\n\n // Focused tab label\n &:focus + label {\n outline-style: auto;\n }\n\n // Disable focus indicator for pointer devices\n &:not(.focus-visible) + label {\n outline: none;\n -webkit-tap-highlight-color: transparent;\n }\n }\n\n // Tab label\n > label {\n z-index: 1;\n width: auto;\n padding: px2em(12px, 12.8px) 1.25em px2em(10px, 12.8px);\n color: var(--md-default-fg-color--light);\n font-weight: 700;\n font-size: px2rem(12.8px);\n border-bottom: px2rem(2px) solid transparent;\n cursor: pointer;\n transition: color 250ms;\n\n // Hovered label\n html &:hover {\n color: var(--md-accent-fg-color);\n }\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Icon definitions\n:root {\n --md-tasklist-icon: svg-load(\n \"@primer/octicons/build/svg/check-circle-fill-24.svg\"\n );\n --md-tasklist-icon--checked: svg-load(\n \"@primer/octicons/build/svg/check-circle-fill-24.svg\"\n );\n}\n\n// ----------------------------------------------------------------------------\n\n// Scoped in typesetted content to match specificity of regular content\n.md-typeset {\n\n // Remove list icon on task items\n .task-list-item {\n position: relative;\n list-style-type: none;\n\n // Make checkbox items align with normal list items, but position\n // everything in ems for correct layout at smaller font sizes\n [type=\"checkbox\"] {\n position: absolute;\n top: 0.45em;\n left: -2em;\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: -2em;\n left: initial;\n }\n }\n }\n\n // Wrapper for list controls, in case custom checkboxes are enabled\n .task-list-control {\n\n // Checkbox icon in unchecked state\n .task-list-indicator::before {\n position: absolute;\n top: 0.15em;\n left: px2em(-24px);\n width: px2em(20px);\n height: px2em(20px);\n background-color: var(--md-default-fg-color--lightest);\n mask-image: var(--md-tasklist-icon);\n mask-repeat: no-repeat;\n content: \"\";\n\n // Adjust for right-to-left languages\n [dir=\"rtl\"] & {\n right: px2em(-24px);\n left: initial;\n }\n }\n\n // Checkbox icon in checked state\n [type=\"checkbox\"]:checked + .task-list-indicator::before {\n background-color: $clr-green-a400;\n mask-image: var(--md-tasklist-icon--checked);\n }\n\n // Hide original checkbox behind icon\n [type=\"checkbox\"] {\n z-index: -1;\n opacity: 0;\n }\n }\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/v2/assets/stylesheets/palette.3f72e892.min.css b/v2/assets/stylesheets/palette.3f72e892.min.css new file mode 100644 index 000000000..0d3745832 --- /dev/null +++ b/v2/assets/stylesheets/palette.3f72e892.min.css @@ -0,0 +1,3 @@ +[data-md-color-accent=red]{--md-accent-fg-color: hsla(348, 100%, 55%, 1);--md-accent-fg-color--transparent: hsla(348, 100%, 55%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=pink]{--md-accent-fg-color: hsla(339, 100%, 48%, 1);--md-accent-fg-color--transparent: hsla(339, 100%, 48%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=purple]{--md-accent-fg-color: hsla(291, 96%, 62%, 1);--md-accent-fg-color--transparent: hsla(291, 96%, 62%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=deep-purple]{--md-accent-fg-color: hsla(256, 100%, 65%, 1);--md-accent-fg-color--transparent: hsla(256, 100%, 65%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=indigo]{--md-accent-fg-color: hsla(231, 99%, 66%, 1);--md-accent-fg-color--transparent: hsla(231, 99%, 66%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=blue]{--md-accent-fg-color: hsla(218, 100%, 63%, 1);--md-accent-fg-color--transparent: hsla(218, 100%, 63%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=light-blue]{--md-accent-fg-color: hsla(203, 100%, 46%, 1);--md-accent-fg-color--transparent: hsla(203, 100%, 46%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=cyan]{--md-accent-fg-color: hsla(188, 100%, 42%, 1);--md-accent-fg-color--transparent: hsla(188, 100%, 42%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=teal]{--md-accent-fg-color: hsla(172, 100%, 37%, 1);--md-accent-fg-color--transparent: hsla(172, 100%, 37%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=green]{--md-accent-fg-color: hsla(145, 100%, 39%, 1);--md-accent-fg-color--transparent: hsla(145, 100%, 39%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=light-green]{--md-accent-fg-color: hsla(97, 81%, 48%, 1);--md-accent-fg-color--transparent: hsla(97, 81%, 48%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-accent=lime]{--md-accent-fg-color: hsla(75, 100%, 46%, 1);--md-accent-fg-color--transparent: hsla(75, 100%, 46%, 0.1);--md-accent-bg-color: hsla(0, 0%, 0%, 0.87);--md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-accent=yellow]{--md-accent-fg-color: hsla(50, 100%, 50%, 1);--md-accent-fg-color--transparent: hsla(50, 100%, 50%, 0.1);--md-accent-bg-color: hsla(0, 0%, 0%, 0.87);--md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-accent=amber]{--md-accent-fg-color: hsla(40, 100%, 50%, 1);--md-accent-fg-color--transparent: hsla(40, 100%, 50%, 0.1);--md-accent-bg-color: hsla(0, 0%, 0%, 0.87);--md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-accent=orange]{--md-accent-fg-color: hsla(34, 100%, 50%, 1);--md-accent-fg-color--transparent: hsla(34, 100%, 50%, 0.1);--md-accent-bg-color: hsla(0, 0%, 0%, 0.87);--md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-accent=deep-orange]{--md-accent-fg-color: hsla(14, 100%, 63%, 1);--md-accent-fg-color--transparent: hsla(14, 100%, 63%, 0.1);--md-accent-bg-color: hsla(0, 0%, 100%, 1);--md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=red]{--md-primary-fg-color: hsla(1, 83%, 63%, 1);--md-primary-fg-color--light: hsla(0, 69%, 67%, 1);--md-primary-fg-color--dark: hsla(1, 77%, 55%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=pink]{--md-primary-fg-color: hsla(340, 82%, 52%, 1);--md-primary-fg-color--light: hsla(340, 82%, 59%, 1);--md-primary-fg-color--dark: hsla(336, 78%, 43%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=purple]{--md-primary-fg-color: hsla(291, 47%, 51%, 1);--md-primary-fg-color--light: hsla(291, 47%, 60%, 1);--md-primary-fg-color--dark: hsla(287, 65%, 40%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=deep-purple]{--md-primary-fg-color: hsla(262, 47%, 55%, 1);--md-primary-fg-color--light: hsla(262, 47%, 63%, 1);--md-primary-fg-color--dark: hsla(262, 52%, 47%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=indigo]{--md-primary-fg-color: hsla(231, 48%, 48%, 1);--md-primary-fg-color--light: hsla(231, 44%, 56%, 1);--md-primary-fg-color--dark: hsla(232, 54%, 41%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=blue]{--md-primary-fg-color: hsla(207, 90%, 54%, 1);--md-primary-fg-color--light: hsla(207, 90%, 61%, 1);--md-primary-fg-color--dark: hsla(210, 79%, 46%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=light-blue]{--md-primary-fg-color: hsla(199, 98%, 48%, 1);--md-primary-fg-color--light: hsla(199, 92%, 56%, 1);--md-primary-fg-color--dark: hsla(201, 98%, 41%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=cyan]{--md-primary-fg-color: hsla(187, 100%, 42%, 1);--md-primary-fg-color--light: hsla(187, 71%, 50%, 1);--md-primary-fg-color--dark: hsla(186, 100%, 33%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=teal]{--md-primary-fg-color: hsla(174, 100%, 29%, 1);--md-primary-fg-color--light: hsla(174, 63%, 40%, 1);--md-primary-fg-color--dark: hsla(173, 100%, 24%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=green]{--md-primary-fg-color: hsla(122, 39%, 49%, 1);--md-primary-fg-color--light: hsla(123, 38%, 57%, 1);--md-primary-fg-color--dark: hsla(123, 43%, 39%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=light-green]{--md-primary-fg-color: hsla(88, 50%, 53%, 1);--md-primary-fg-color--light: hsla(88, 50%, 60%, 1);--md-primary-fg-color--dark: hsla(92, 48%, 42%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=lime]{--md-primary-fg-color: hsla(66, 70%, 54%, 1);--md-primary-fg-color--light: hsla(66, 70%, 61%, 1);--md-primary-fg-color--dark: hsla(62, 61%, 44%, 1);--md-primary-bg-color: hsla(0, 0%, 0%, 0.87);--md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-primary=yellow]{--md-primary-fg-color: hsla(54, 100%, 62%, 1);--md-primary-fg-color--light: hsla(54, 100%, 67%, 1);--md-primary-fg-color--dark: hsla(43, 96%, 58%, 1);--md-primary-bg-color: hsla(0, 0%, 0%, 0.87);--md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-primary=amber]{--md-primary-fg-color: hsla(45, 100%, 51%, 1);--md-primary-fg-color--light: hsla(45, 100%, 58%, 1);--md-primary-fg-color--dark: hsla(38, 100%, 50%, 1);--md-primary-bg-color: hsla(0, 0%, 0%, 0.87);--md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-primary=orange]{--md-primary-fg-color: hsla(36, 100%, 57%, 1);--md-primary-fg-color--light: hsla(36, 100%, 57%, 1);--md-primary-fg-color--dark: hsla(33, 100%, 49%, 1);--md-primary-bg-color: hsla(0, 0%, 0%, 0.87);--md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54)}[data-md-color-primary=deep-orange]{--md-primary-fg-color: hsla(14, 100%, 63%, 1);--md-primary-fg-color--light: hsla(14, 100%, 70%, 1);--md-primary-fg-color--dark: hsla(14, 91%, 54%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=brown]{--md-primary-fg-color: hsla(16, 25%, 38%, 1);--md-primary-fg-color--light: hsla(16, 18%, 47%, 1);--md-primary-fg-color--dark: hsla(14, 26%, 29%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=grey]{--md-primary-fg-color: hsla(0, 0%, 46%, 1);--md-primary-fg-color--light: hsla(0, 0%, 62%, 1);--md-primary-fg-color--dark: hsla(0, 0%, 38%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=blue-grey]{--md-primary-fg-color: hsla(199, 18%, 40%, 1);--md-primary-fg-color--light: hsla(200, 18%, 46%, 1);--md-primary-fg-color--dark: hsla(199, 18%, 33%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7)}[data-md-color-primary=white]{--md-primary-fg-color: hsla(0, 0%, 100%, 1);--md-primary-fg-color--light: hsla(0, 0%, 100%, 0.7);--md-primary-fg-color--dark: hsla(0, 0%, 0%, 0.07);--md-primary-bg-color: hsla(0, 0%, 0%, 0.87);--md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54);--md-typeset-a-color: hsla(231, 48%, 48%, 1)}@media screen and (min-width: 60em){[data-md-color-primary=white] .md-search__input{background-color:rgba(0,0,0,.07)}[data-md-color-primary=white] .md-search__input+.md-search__icon{color:rgba(0,0,0,.87)}[data-md-color-primary=white] .md-search__input::-webkit-input-placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input::-moz-placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input::-ms-input-placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input::placeholder{color:rgba(0,0,0,.54)}[data-md-color-primary=white] .md-search__input:hover{background-color:rgba(0,0,0,.32)}}@media screen and (min-width: 76.25em){[data-md-color-primary=white] .md-tabs{border-bottom:.05rem solid rgba(0,0,0,.07)}}[data-md-color-primary=black]{--md-primary-fg-color: hsla(0, 0%, 0%, 1);--md-primary-fg-color--light: hsla(0, 0%, 0%, 0.54);--md-primary-fg-color--dark: hsla(0, 0%, 0%, 1);--md-primary-bg-color: hsla(0, 0%, 100%, 1);--md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);--md-typeset-a-color: hsla(231, 48%, 48%, 1)}[data-md-color-primary=black] .md-header{background-color:#000}@media screen and (max-width: 59.9375em){[data-md-color-primary=black] .md-nav__source{background-color:rgba(0,0,0,.87)}}@media screen and (min-width: 60em){[data-md-color-primary=black] .md-search__input{background-color:rgba(255,255,255,.12)}[data-md-color-primary=black] .md-search__input:hover{background-color:rgba(255,255,255,.3)}}@media screen and (max-width: 76.1875em){html [data-md-color-primary=black] .md-nav--primary .md-nav__title[for=__drawer]{background-color:#000}}@media screen and (min-width: 76.25em){[data-md-color-primary=black] .md-tabs{background-color:#000}}[data-md-color-scheme=slate]{--md-hue: 232;--md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1);--md-default-fg-color--light: hsla(var(--md-hue), 75%, 90%, 0.62);--md-default-fg-color--lighter: hsla(var(--md-hue), 75%, 90%, 0.32);--md-default-fg-color--lightest: hsla(var(--md-hue), 75%, 90%, 0.12);--md-default-bg-color: hsla(var(--md-hue), 15%, 21%, 1);--md-default-bg-color--light: hsla(var(--md-hue), 15%, 21%, 0.54);--md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 21%, 0.26);--md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 21%, 0.07);--md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 1);--md-code-bg-color: hsla(var(--md-hue), 15%, 15%, 1);--md-code-hl-color: hsla(218, 100%, 63%, 0.15);--md-code-hl-number-color: hsla(6, 74%, 63%, 1);--md-code-hl-special-color: hsla(340, 83%, 66%, 1);--md-code-hl-function-color: hsla(291, 57%, 65%, 1);--md-code-hl-constant-color: hsla(250, 62%, 70%, 1);--md-code-hl-keyword-color: hsla(219, 66%, 64%, 1);--md-code-hl-string-color: hsla(150, 58%, 44%, 1);--md-typeset-a-color: var(--md-primary-fg-color--light);--md-typeset-mark-color: hsla(218, 100%, 63%, 0.3);--md-typeset-kbd-color: hsla(var(--md-hue), 15%, 94%, 0.12);--md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 94%, 0.2);--md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1);--md-admonition-bg-color: hsla(var(--md-hue), 0%, 100%, 0.025);--md-footer-bg-color: hsla(var(--md-hue), 15%, 12%, 0.87);--md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 10%, 1)} + +/*# sourceMappingURL=palette.3f72e892.min.css.map*/ \ No newline at end of file diff --git a/v2/assets/stylesheets/palette.3f72e892.min.css.map b/v2/assets/stylesheets/palette.3f72e892.min.css.map new file mode 100644 index 000000000..8d3158622 --- /dev/null +++ b/v2/assets/stylesheets/palette.3f72e892.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///src/assets/stylesheets/palette/_accent.scss","webpack:///src/assets/stylesheets/palette/_primary.scss","webpack:///src/assets/stylesheets/utilities/_break.scss","webpack:///src/assets/stylesheets/palette/_scheme.scss"],"names":[],"mappings":"AA8CE,2BACE,8CACA,6DAOE,2CACA,oDAVJ,4BACE,8CACA,6DAOE,2CACA,oDAVJ,8BACE,6CACA,4DAOE,2CACA,oDAVJ,mCACE,8CACA,6DAOE,2CACA,oDAVJ,8BACE,6CACA,4DAOE,2CACA,oDAVJ,4BACE,8CACA,6DAOE,2CACA,oDAVJ,kCACE,8CACA,6DAOE,2CACA,oDAVJ,4BACE,8CACA,6DAOE,2CACA,oDAVJ,4BACE,8CACA,6DAOE,2CACA,oDAVJ,6BACE,8CACA,6DAOE,2CACA,oDAVJ,mCACE,4CACA,2DAOE,2CACA,oDAVJ,4BACE,6CACA,4DAIE,4CACA,mDAPJ,8BACE,6CACA,4DAIE,4CACA,mDAPJ,6BACE,6CACA,4DAIE,4CACA,mDAPJ,8BACE,6CACA,4DAIE,4CACA,mDAPJ,mCACE,6CACA,4DAOE,2CACA,oDCPJ,4BACE,4CACA,mDACA,kDAOE,4CACA,qDAXJ,6BACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,+BACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,oCACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,+BACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,6BACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,mCACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,6BACE,+CACA,qDACA,qDAOE,4CACA,qDAXJ,6BACE,+CACA,qDACA,qDAOE,4CACA,qDAXJ,8BACE,8CACA,qDACA,oDAOE,4CACA,qDAXJ,oCACE,6CACA,oDACA,mDAOE,4CACA,qDAXJ,6BACE,6CACA,oDACA,mDAIE,6CACA,oDARJ,+BACE,8CACA,qDACA,mDAIE,6CACA,oDARJ,8BACE,8CACA,qDACA,oDAIE,6CACA,oDARJ,+BACE,8CACA,qDACA,oDAIE,6CACA,oDARJ,oCACE,8CACA,qDACA,mDAOE,4CACA,qDAXJ,8BACE,6CACA,oDACA,mDAOE,4CACA,qDAXJ,6BACE,2CACA,kDACA,iDAOE,4CACA,qDAXJ,kCACE,8CACA,qDACA,oDAOE,4CACA,qDAUN,8BACE,4CACA,qDACA,mDACA,6CACA,oDAGA,6CC6GE,oCDvGA,gDACE,iCAGA,iEACE,sBAIF,2EACE,sBADF,kEACE,sBADF,uEACE,sBADF,6DACE,sBAIF,sDACE,kCCwFJ,uCD/EA,uCACE,4CAUN,8BACE,0CACA,oDACA,gDACA,4CACA,qDAGA,6CAGA,yCACE,sBC0EA,yCDnEA,8CACE,kCCgDF,oCDxCA,gDACE,uCAGA,sDACE,uCCqDJ,yCD5CA,iFACE,uBCyBF,uCDjBA,uCACE,uBEhJN,6BAKE,cAGA,wDACA,kEACA,oEACA,qEACA,wDACA,kEACA,oEACA,qEAGA,qDACA,qDAGA,+CACA,gDACA,mDACA,oDACA,oDACA,mDACA,kDAGA,wDAGA,mDAGA,4DACA,kEACA,gEAGA,+DAGA,0DACA,6D","file":"assets/stylesheets/palette.3f72e892.min.css","sourcesContent":["////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n@each $name, $color in (\n \"red\": $clr-red-a400,\n \"pink\": $clr-pink-a400,\n \"purple\": $clr-purple-a200,\n \"deep-purple\": $clr-deep-purple-a200,\n \"indigo\": $clr-indigo-a200,\n \"blue\": $clr-blue-a200,\n \"light-blue\": $clr-light-blue-a700,\n \"cyan\": $clr-cyan-a700,\n \"teal\": $clr-teal-a700,\n \"green\": $clr-green-a700,\n \"light-green\": $clr-light-green-a700,\n \"lime\": $clr-lime-a700,\n \"yellow\": $clr-yellow-a700,\n \"amber\": $clr-amber-a700,\n \"orange\": $clr-orange-a400,\n \"deep-orange\": $clr-deep-orange-a200\n) {\n\n // Color palette\n [data-md-color-accent=\"#{$name}\"] {\n --md-accent-fg-color: hsla(#{hex2hsl($color)}, 1);\n --md-accent-fg-color--transparent: hsla(#{hex2hsl($color)}, 0.1);\n\n // Inverted text for lighter shades\n @if index(\"lime\" \"yellow\" \"amber\" \"orange\", $name) {\n --md-accent-bg-color: hsla(0, 0%, 0%, 0.87);\n --md-accent-bg-color--light: hsla(0, 0%, 0%, 0.54);\n } @else {\n --md-accent-bg-color: hsla(0, 0%, 100%, 1);\n --md-accent-bg-color--light: hsla(0, 0%, 100%, 0.7);\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n@each $name, $colors in (\n \"red\": $clr-red-400 $clr-red-300 $clr-red-600,\n \"pink\": $clr-pink-500 $clr-pink-400 $clr-pink-700,\n \"purple\": $clr-purple-400 $clr-purple-300 $clr-purple-600,\n \"deep-purple\": $clr-deep-purple-400 $clr-deep-purple-300 $clr-deep-purple-500,\n \"indigo\": $clr-indigo-500 $clr-indigo-400 $clr-indigo-700,\n \"blue\": $clr-blue-500 $clr-blue-400 $clr-blue-700,\n \"light-blue\": $clr-light-blue-500 $clr-light-blue-400 $clr-light-blue-700,\n \"cyan\": $clr-cyan-500 $clr-cyan-400 $clr-cyan-700,\n \"teal\": $clr-teal-500 $clr-teal-400 $clr-teal-700,\n \"green\": $clr-green-500 $clr-green-400 $clr-green-700,\n \"light-green\": $clr-light-green-500 $clr-light-green-400 $clr-light-green-700,\n \"lime\": $clr-lime-500 $clr-lime-400 $clr-lime-700,\n \"yellow\": $clr-yellow-500 $clr-yellow-400 $clr-yellow-700,\n \"amber\": $clr-amber-500 $clr-amber-400 $clr-amber-700,\n \"orange\": $clr-orange-400 $clr-orange-400 $clr-orange-600,\n \"deep-orange\": $clr-deep-orange-400 $clr-deep-orange-300 $clr-deep-orange-600,\n \"brown\": $clr-brown-500 $clr-brown-400 $clr-brown-700,\n \"grey\": $clr-grey-600 $clr-grey-500 $clr-grey-700,\n \"blue-grey\": $clr-blue-grey-600 $clr-blue-grey-500 $clr-blue-grey-700\n) {\n\n // Color palette\n [data-md-color-primary=\"#{$name}\"] {\n --md-primary-fg-color: hsla(#{hex2hsl(nth($colors, 1))}, 1);\n --md-primary-fg-color--light: hsla(#{hex2hsl(nth($colors, 2))}, 1);\n --md-primary-fg-color--dark: hsla(#{hex2hsl(nth($colors, 3))}, 1);\n\n // Inverted text for lighter shades\n @if index(\"lime\" \"yellow\" \"amber\" \"orange\", $name) {\n --md-primary-bg-color: hsla(0, 0%, 0%, 0.87);\n --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54);\n } @else {\n --md-primary-bg-color: hsla(0, 0%, 100%, 1);\n --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);\n }\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules: white\n// ----------------------------------------------------------------------------\n\n// Color palette\n[data-md-color-primary=\"white\"] {\n --md-primary-fg-color: hsla(0, 0%, 100%, 1);\n --md-primary-fg-color--light: hsla(0, 0%, 100%, 0.7);\n --md-primary-fg-color--dark: hsla(0, 0%, 0%, 0.07);\n --md-primary-bg-color: hsla(0, 0%, 0%, 0.87);\n --md-primary-bg-color--light: hsla(0, 0%, 0%, 0.54);\n\n // Typeset color shades\n --md-typeset-a-color: hsla(#{hex2hsl($clr-indigo-500)}, 1);\n\n // [tablet portrait +]: Change color of search input\n @include break-from-device(tablet landscape) {\n\n // Search input\n .md-search__input {\n background-color: hsla(0, 0%, 0%, 0.07);\n\n // Search icon color\n + .md-search__icon {\n color: hsla(0, 0%, 0%, 0.87);\n }\n\n // Placeholder color\n &::placeholder {\n color: hsla(0, 0%, 0%, 0.54);\n }\n\n // Hovered search field\n &:hover {\n background-color: hsla(0, 0%, 0%, 0.32);\n }\n }\n }\n\n // [screen +]: Set bottom border for tabs\n @include break-from-device(screen) {\n\n // Tabs with outline\n .md-tabs {\n border-bottom: px2rem(1px) solid hsla(0, 0%, 0%, 0.07);\n }\n }\n}\n\n// ----------------------------------------------------------------------------\n// Rules: black\n// ----------------------------------------------------------------------------\n\n// Color palette\n[data-md-color-primary=\"black\"] {\n --md-primary-fg-color: hsla(0, 0%, 0%, 1);\n --md-primary-fg-color--light: hsla(0, 0%, 0%, 0.54);\n --md-primary-fg-color--dark: hsla(0, 0%, 0%, 1);\n --md-primary-bg-color: hsla(0, 0%, 100%, 1);\n --md-primary-bg-color--light: hsla(0, 0%, 100%, 0.7);\n\n // Text color shades\n --md-typeset-a-color: hsla(#{hex2hsl($clr-indigo-500)}, 1);\n\n // Application header (stays always on top)\n .md-header {\n background-color: hsla(0, 0%, 0%, 1);\n }\n\n // [tablet portrait -]: Layered navigation\n @include break-to-device(tablet portrait) {\n\n // Repository containing source\n .md-nav__source {\n background-color: hsla(0, 0%, 0%, 0.87);\n }\n }\n\n // [tablet landscape +]: Header-embedded search\n @include break-from-device(tablet landscape) {\n\n // Search input\n .md-search__input {\n background-color: hsla(0, 0%, 100%, 0.12);\n\n // Hovered search field\n &:hover {\n background-color: hsla(0, 0%, 100%, 0.3);\n }\n }\n }\n\n // [tablet -]: Layered navigation\n @include break-to-device(tablet) {\n\n // Site title in main navigation\n html & .md-nav--primary .md-nav__title[for=\"__drawer\"] {\n background-color: hsla(0, 0%, 0%, 1);\n }\n }\n\n // [screen +]: Set background color for tabs\n @include break-from-device(screen) {\n\n // Tabs with outline\n .md-tabs {\n background-color: hsla(0, 0%, 0%, 1);\n }\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Variables\n// ----------------------------------------------------------------------------\n\n///\n/// Device-specific breakpoints\n///\n/// @example\n/// $break-devices: (\n/// mobile: (\n/// portrait: 220px 479px,\n/// landscape: 480px 719px\n/// ),\n/// tablet: (\n/// portrait: 720px 959px,\n/// landscape: 960px 1219px\n/// ),\n/// screen: (\n/// small: 1220px 1599px,\n/// medium: 1600px 1999px,\n/// large: 2000px\n/// )\n/// );\n///\n$break-devices: () !default;\n\n// ----------------------------------------------------------------------------\n// Helpers\n// ----------------------------------------------------------------------------\n\n///\n/// Choose minimum and maximum device widths\n///\n@function break-select-min-max($devices) {\n $min: 1000000;\n $max: 0;\n @each $key, $value in $devices {\n @while type-of($value) == map {\n $value: break-select-min-max($value);\n }\n @if type-of($value) == list {\n @each $number in $value {\n @if type-of($number) == number {\n $min: min($number, $min);\n @if $max != null {\n $max: max($number, $max);\n }\n } @else {\n @error \"Invalid number: #{$number}\";\n }\n }\n } @else if type-of($value) == number {\n $min: min($value, $min);\n $max: null;\n } @else {\n @error \"Invalid value: #{$value}\";\n }\n }\n @return $min, $max;\n}\n\n///\n/// Select minimum and maximum widths for a device breakpoint\n///\n@function break-select-device($device) {\n $current: $break-devices;\n @for $n from 1 through length($device) {\n @if type-of($current) == map {\n $current: map-get($current, nth($device, $n));\n } @else {\n @error \"Invalid device map: #{$devices}\";\n }\n }\n @if type-of($current) == list or type-of($current) == number {\n $current: (default: $current);\n }\n @return break-select-min-max($current);\n}\n\n// ----------------------------------------------------------------------------\n// Mixins\n// ----------------------------------------------------------------------------\n\n///\n/// A minimum-maximum media query breakpoint\n///\n@mixin break-at($breakpoint) {\n @if type-of($breakpoint) == number {\n @media screen and (min-width: $breakpoint) {\n @content;\n }\n } @else if type-of($breakpoint) == list {\n $min: nth($breakpoint, 1);\n $max: nth($breakpoint, 2);\n @if type-of($min) == number and type-of($max) == number {\n @media screen and (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n}\n\n///\n/// An orientation media query breakpoint\n///\n@mixin break-at-orientation($breakpoint) {\n @if type-of($breakpoint) == string {\n @media screen and (orientation: $breakpoint) {\n @content;\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n}\n\n///\n/// A maximum-aspect-ratio media query breakpoint\n///\n@mixin break-at-ratio($breakpoint) {\n @if type-of($breakpoint) == number {\n @media screen and (max-aspect-ratio: $breakpoint) {\n @content;\n }\n } @else {\n @error \"Invalid breakpoint: #{$breakpoint}\";\n }\n}\n\n///\n/// A minimum-maximum media query device breakpoint\n///\n@mixin break-at-device($device) {\n @if type-of($device) == string {\n $device: $device,;\n }\n @if type-of($device) == list {\n $breakpoint: break-select-device($device);\n @if nth($breakpoint, 2) != null {\n $min: nth($breakpoint, 1);\n $max: nth($breakpoint, 2);\n @media screen and (min-width: $min) and (max-width: $max) {\n @content;\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n}\n\n///\n/// A minimum media query device breakpoint\n///\n@mixin break-from-device($device) {\n @if type-of($device) == string {\n $device: $device,;\n }\n @if type-of($device) == list {\n $breakpoint: break-select-device($device);\n $min: nth($breakpoint, 1);\n @media screen and (min-width: $min) {\n @content;\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n}\n\n///\n/// A maximum media query device breakpoint\n///\n@mixin break-to-device($device) {\n @if type-of($device) == string {\n $device: $device,;\n }\n @if type-of($device) == list {\n $breakpoint: break-select-device($device);\n $max: nth($breakpoint, 2);\n @media screen and (max-width: $max) {\n @content;\n }\n } @else {\n @error \"Invalid device: #{$device}\";\n }\n}\n","////\n/// Copyright (c) 2016-2020 Martin Donath \n///\n/// Permission is hereby granted, free of charge, to any person obtaining a\n/// copy of this software and associated documentation files (the \"Software\"),\n/// to deal in the Software without restriction, including without limitation\n/// the rights to use, copy, modify, merge, publish, distribute, sublicense,\n/// and/or sell copies of the Software, and to permit persons to whom the\n/// Software is furnished to do so, subject to the following conditions:\n///\n/// The above copyright notice and this permission notice shall be included in\n/// all copies or substantial portions of the Software.\n///\n/// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n/// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL\n/// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n/// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n/// DEALINGS\n////\n\n// ----------------------------------------------------------------------------\n// Rules\n// ----------------------------------------------------------------------------\n\n// Slate theme, i.e. dark mode\n[data-md-color-scheme=\"slate\"] {\n\n // Slate's hue in the range [0,360] - change this variable to alter the tone\n // of the theme, e.g. to make it more redish or greenish. This is a slate-\n // specific variable, but the same approach may be adapted to custom themes.\n --md-hue: 232;\n\n // Default color shades\n --md-default-fg-color: hsla(var(--md-hue), 75%, 95%, 1);\n --md-default-fg-color--light: hsla(var(--md-hue), 75%, 90%, 0.62);\n --md-default-fg-color--lighter: hsla(var(--md-hue), 75%, 90%, 0.32);\n --md-default-fg-color--lightest: hsla(var(--md-hue), 75%, 90%, 0.12);\n --md-default-bg-color: hsla(var(--md-hue), 15%, 21%, 1);\n --md-default-bg-color--light: hsla(var(--md-hue), 15%, 21%, 0.54);\n --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 21%, 0.26);\n --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 21%, 0.07);\n\n // Code color shades\n --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 1);\n --md-code-bg-color: hsla(var(--md-hue), 15%, 15%, 1);\n\n // Code highlighting color shades\n --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.15);\n --md-code-hl-number-color: hsla(6, 74%, 63%, 1);\n --md-code-hl-special-color: hsla(340, 83%, 66%, 1);\n --md-code-hl-function-color: hsla(291, 57%, 65%, 1);\n --md-code-hl-constant-color: hsla(250, 62%, 70%, 1);\n --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1);\n --md-code-hl-string-color: hsla(150, 58%, 44%, 1);\n\n // Typeset color shades\n --md-typeset-a-color: var(--md-primary-fg-color--light);\n\n // Typeset `mark` color shades\n --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3);\n\n // Typeset `kbd` color shades\n --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 94%, 0.12);\n --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 94%, 0.2);\n --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1);\n\n // Admonition color shades\n --md-admonition-bg-color: hsla(var(--md-hue), 0%, 100%, 0.025);\n\n // Footer color shades\n --md-footer-bg-color: hsla(var(--md-hue), 15%, 12%, 0.87);\n --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 10%, 1);\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/v2/authentication/adaljsclient/index.html b/v2/authentication/adaljsclient/index.html new file mode 100644 index 000000000..da0464104 --- /dev/null +++ b/v2/authentication/adaljsclient/index.html @@ -0,0 +1,2510 @@ + + + + + + + + + + + + + + + + + + ADAL Client - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/adalclient

    +

    This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions.

    +
    +

    Where possible it is recommended to use the MSAL client.

    +
    +

    Getting Started

    +

    Install the library and required dependencies

    +

    npm install @pnp/adaljsclient --save

    +

    Setup and Use inside SharePoint Framework

    +

    Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes. This method will only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined below using the constructor to specify the values for an AAD Application you have setup.

    +

    Calling the graph api

    +

    By providing the context in the onInit we can create the adal client from known information.

    +
    import { graph } from "@pnp/graph";
    +import { getRandomString } from "@pnp/core";
    +
    +// ...
    +
    +public onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +    graph.setup(this.context);
    +  });
    +}
    +
    +public render(): void {
    +
    +  // here we are creating a team with a random name, required Group ReadWrite All permissions
    +  const teamName = `ATeam.${getRandomString(4)}`;
    +
    +  this.domElement.innerHTML = `Hello, I am creating a team named "${teamName}" for you...`;
    +
    +  graph.teams.create(teamName, "This is a description").then(t => {
    +
    +    this.domElement.innerHTML += "done!";
    +
    +  }).catch(e => {
    +
    +    this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`;
    +  });
    +}
    +
    +

    Calling the SharePoint API

    +

    This example shows how to use the ADALClient with the @pnp/sp library to call an API secured with AAD from within SharePoint Framework.

    +
    import { SPFxAdalClient } from "@pnp/core";
    +import { sp } from "@pnp/sp/presets/all";
    +
    +// ...
    +
    +public onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +    sp.setup({
    +      spfxContext: this.context,
    +      sp: {
    +        fetchClientFactory: () => new SPFxAdalClient(this.context),
    +      },
    +    });
    +
    +  });
    +}
    +
    +public render(): void {
    +
    +  sp.web().then(t => {
    +    this.domElement.innerHTML = JSON.stringify(t);
    +  }).catch(e => {
    +    this.domElement.innerHTML = JSON.stringify(e);
    +  });
    +}
    +
    +

    Calling the any API

    +

    You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example.

    +
    import { FetchOptions } from "@pnp/core";
    +import { AdalClient } from "@pnp/adaljsclient";
    +import { ODataDefaultParser } from "@pnp/queryable";
    +
    +// ...
    +
    +public render(): void {
    +
    +  // create an ADAL Client
    +  const client = AdalClient.fromSPFxContext(this.context);
    +
    +  // setup the request options
    +  const opts: FetchOptions = {
    +    method: "GET",
    +    headers: {
    +      "Accept": "application/json",
    +    },
    +  };
    +
    +  // execute the request
    +  client.fetch("https://{tenant}.sharepoint.com/_api/web", opts).then(response => {
    +
    +    // create a parser to convert the response into JSON.
    +    // You can create your own, at this point you have a fetch Response to work with
    +    const parser = new ODataDefaultParser();
    +
    +    parser.parse(response).then(json => {
    +      this.domElement.innerHTML = JSON.stringify(json);
    +    });
    +
    +  }).catch(e => {
    +    this.domElement.innerHTML = JSON.stringify(e);
    +  });
    +
    +}
    +
    +

    Manually Configure

    +

    This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD.

    +

    Setup and Use with Microsoft Graph

    +

    This sample uses a custom AzureAd app you have created and granted the appropriate permissions.

    +
    import { AdalClient } from "@pnp/adaljsclient";
    +import { graph } from "@pnp/graph";
    +
    +// configure the graph client
    +// parameters are:
    +// client id - the id of the application you created in azure ad
    +// tenant - can be id or URL (shown)
    +// redirect url - absolute url of a page to which your application and Azure AD app allows replies
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +            return new AdalClient(
    +                "00000000-0000-0000-0000-000000000000",
    +                "{tenant}.onmicrosoft.com",
    +                "https://myapp/singlesignon.aspx");
    +        },
    +    },
    +});
    +
    +try {
    +
    +    // call the graph API
    +    const groups = await graph.groups();
    +
    +    console.log(JSON.stringify(groups, null, 4));
    +
    +} catch (e) {
    +    console.error(e);
    +}
    +
    +

    Nodejs Applications

    +

    We have a dedicated node client in @pnp/nodejs.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/bearertokenclient/index.html b/v2/authentication/bearertokenclient/index.html new file mode 100644 index 000000000..9b1498120 --- /dev/null +++ b/v2/authentication/bearertokenclient/index.html @@ -0,0 +1,2251 @@ + + + + + + + + + + + + + + + + + + Bearer Token Client - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/BearerTokenFetchClient

    +

    The BearerTokenFetchClient takes a single parameter representing an access token and uses it to make the requests.

    +
    +

    The disadvantage to this approach is not knowing to where the request will be sent, which in some cases is fine. An alternative is the LambdaFetchClient

    +
    +

    Static

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import { BearerTokenFetchClient } from "@pnp/core";
    +import { myTokenFactory } from "./my-auth.js";
    +
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +
    +            // note this method is not async, so your logic here cannot await.
    +            // Please see the LambdaFetchClient if you have a need for async support.
    +            const token = myTokenFactory();
    +            return new BearerTokenFetchClient(token);
    +        },
    +    },
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/client-spa/index.html b/v2/authentication/client-spa/index.html new file mode 100644 index 000000000..6efdf446f --- /dev/null +++ b/v2/authentication/client-spa/index.html @@ -0,0 +1,2206 @@ + + + + + + + + + + + + + + + + + + SPA Auth - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Authentication in Single Page Application

    +

    If you are writing a single page application deployed outside SharePoint it is recommended to use the MSAL client. You can find further details on the settings in the MSAL docs. You will need to ensure that you grant the permissions required to the application you are trying to use.

    +
    import { MsalClientSetup  } from "@pnp/msaljsclient";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: MsalClientSetup({
    +            auth: {
    +                authority: "https://login.microsoftonline.com/common",
    +                clientId: "00000000-0000-0000-0000-000000000000",
    +                redirectUri: "{your redirect uri}",
    +            },
    +            cache: {
    +                cacheLocation: "sessionStorage",
    +            },
    +        }, ["email", "Files.Read.All", "User.Read.All"]),
    +    },
    +});
    +
    +const data = await graph.me();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/client-spfx/index.html b/v2/authentication/client-spfx/index.html new file mode 100644 index 000000000..25ada5bb2 --- /dev/null +++ b/v2/authentication/client-spfx/index.html @@ -0,0 +1,2397 @@ + + + + + + + + + + + + + + + + + + SPFx Auth - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Authentication in SharePoint Framework

    +

    Auth as Current User

    +

    PnPjs is designed to work as easily as possible within the SharePoint Framework so the authentication setup is very simple for the base case. Supply the current SharePoint Framework context to the library. This works for both SharePoint authentication and Graph authentication using the current user. Graph permissions are controlled by the permissions granted to the SharePoint shared application within your tenant.

    +

    The below example is taken from a SharePoint Framework webpart.

    +

    Connect to SharePoint as Current User

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +// ...
    +
    +protected async onInit(): Promise<void> {
    +
    +  await super.onInit();
    +
    +  // other init code may be present
    +
    +  sp.setup(this.context);
    +}
    +
    +// ...
    +
    +

    Connect to Graph as Current User

    +

    Permissions for this graph connection are controlled by the Shared SharePoint Application. You can target other applications using the MSAL Client.

    +
    import { graph } from "@pnp/graph/presets/all";
    +
    +// ...
    +
    +protected async onInit(): Promise<void> {
    +
    +  await super.onInit();
    +
    +  // other init code may be present
    +
    +  // this will use the ADAL client behind the scenes with no additional configuration work
    +  graph.setup(this.context);
    +}
    +
    +// ...
    +
    +

    MSAL Client

    +

    You might want/need to use a client configured to use your own AAD application and not the shared SharePoint application. You can do so using the MSAL client. Here we show this using graph, this works the same with any of the setup strategies. Please see the MSAL library docs for more details on what values to supply in the configuration.

    +
    +

    Note: you must install the @pnp/msaljsclient client package before using it

    +
    +
    import { MsalClientSetup  } from "@pnp/msaljsclient";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +// ...
    +
    +protected async onInit(): Promise<void> {
    +
    +  await super.onInit();
    +
    +  // other init code may be present
    +
    +  graph.setup({
    +      graph: {
    +          fetchClientFactory: MsalClientSetup({
    +              auth: {
    +                  authority: "https://login.microsoftonline.com/common",
    +                  clientId: "00000000-0000-0000-0000-000000000000",
    +                  redirectUri: "{your redirect uri}",
    +              },
    +              cache: {
    +                  cacheLocation: "sessionStorage",
    +              },
    +          }, ["email", "Files.Read.All", "User.Read.All"]),
    +      },
    +  });
    +}
    +
    +// ...
    +
    +

    ADAL Client

    +

    You can use the ADAL client from within SPFx, though it is recommended to transition to the MSAL client.

    +
    +

    Note: you must install the @pnp/adaljsclient client package before using it

    +
    +
    import { AdalClient  } from "@pnp/adaljsclient";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +// ...
    +
    +protected async onInit(): Promise<void> {
    +
    +  await super.onInit();
    +
    +  // other init code may be present
    +
    +  graph.setup({
    +      graph: {
    +          fetchClientFactory: () => {
    +            return new AdalClient(
    +                "00000000-0000-0000-0000-000000000000",
    +                "{tenant}.onmicrosoft.com",
    +                "");
    +          },
    +  });
    +}
    +
    +// ...
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/index.html b/v2/authentication/index.html new file mode 100644 index 000000000..ef50de4d8 --- /dev/null +++ b/v2/authentication/index.html @@ -0,0 +1,2307 @@ + + + + + + + + + + + + + + + + + + Getting Started - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Authentication

    +

    One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods.

    +

    There are two places the PnPjs libraries can be used to connect to various services client (browser) or server.

    +

    Utility Scenarios

    + +

    Client Scenarios

    + +

    Server Scenarios

    + + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/lambdaclient/index.html b/v2/authentication/lambdaclient/index.html new file mode 100644 index 000000000..21c271eb1 --- /dev/null +++ b/v2/authentication/lambdaclient/index.html @@ -0,0 +1,2304 @@ + + + + + + + + + + + + + + + + + + Lambda Token Client - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/LambdaFetchClient

    +

    The LambdaFetchClient class allows you to provide an async function that returns an access token using any logic/supporting libraries you need. This provides total freedom to define how you do authentication, so long as it results in a usable Bearer token to call the target resource. The advantage to the LambdaFetchClient is that you get the url for each request, meaning your logic can account for where the request is headed.

    +
    +

    The token function should be as efficient as possible as it's logic must complete before each request will be sent.

    +
    +

    Signature

    +

    The LambdaFetchClient accepts a single argument of type ILambdaTokenFactoryParams.

    +
    // signature of method, the return string is the access token
    +(parms: ILambdaTokenFactoryParams) => Promise<string>
    +
    +// ILambdaTokenFactoryParams
    +export interface ILambdaTokenFactoryParams {
    +    /**
    +     * Url to which the request for which we are requesting a token will be sent
    +     */
    +    url: string;
    +    /**
    +     * Any options supplied for the request
    +     */
    +    options: IFetchOptions;
    +}
    +
    +

    @azure/msal-browser example

    +

    This example shows how to use @azure/msal-browser along with LambdaFetchClient to achieve signin. msal-browser has many possible configurations which are described within their documentation.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import { LambdaFetchClient } from "@pnp/core";
    +import { PublicClientApplication, Configuration } from "@azure/msal-browser";
    +
    +const config: Configuration = {
    +    auth: {
    +        clientId: "{client id}",
    +        authority: "https://login.microsoftonline.com/common/"
    +    }
    +}
    +
    +// create a single application, could also create this within the lambda client, but it would create a new applicaiton per request
    +const msal = new PublicClientApplication(config);
    +
    +// create a new instance of the lambda fetch client
    +const client = new LambdaFetchClient(async () => {
    +
    +    const request = {
    +        scopes: ["User.Read.All"],
    +    };
    +
    +    const response = await msal.loginPopup(request);
    +
    +    // lamba returns the access token
    +    return response.accessToken;
    +});
    +
    +// setup graph with the client
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => client,
    +    },
    +});
    +
    +// execute the request to graph which will use the client defined above
    +const result = await graph.users();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/msaljsclient/index.html b/v2/authentication/msaljsclient/index.html new file mode 100644 index 000000000..3df1f5a4d --- /dev/null +++ b/v2/authentication/msaljsclient/index.html @@ -0,0 +1,2413 @@ + + + + + + + + + + + + + + + + + + MSAL Client - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    msaljsclient - MSAL Client for PnPjs

    +

    The MSAL client is a thin wrapper around the MSAL library adapting it for use with PnPjs's request pipeline.

    +

    Install

    +

    You need to install the MSAL client before using it. This is in addition to installing the other PnPjs libraries you require.

    +

    npm install @pnp/msaljsclient --save

    +

    Configure

    +

    The PnP client is a very thin wrapper around the MSAL library and you can supply any of the arguments supported. These are described in the MSAL docs.

    +

    The basic configuration values you need (at least from our testing) are client id, authority, and redirectUri. The other options are settable but not required. This article is not intended to be an exhaustive discussion of all the MSAL configuration possibilities, please see the official docs to understand all of the available options.

    +

    The second parameter when configuring the PnP client is the list of scope you are seeking to use. These must be configured and properly granted within AAD and you can request one or more scopes as needed for the current scenario.

    +

    Use in SPFx

    +

    Calling SharePoint via MSAL

    +
    +

    When calling the SharePoint REST API we must use only a special scope "https://{tenant}.sharepoint.com/.default"

    +
    +
    import { MsalClientSetup  } from "@pnp/msaljsclient";
    +import { sp } from "@pnp/sp/presets/all";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: MsalClientSetup({
    +            auth: {
    +                authority: "https://login.microsoftonline.com/mytentant.onmicrosoft.com/",
    +                clientId: "00000000-0000-0000-0000-000000000000",
    +                redirectUri: "https://mytentant.sharepoint.com/sites/dev/SitePages/test.aspx",
    +            },
    +        }, ["https://mytentant.sharepoint.com/.default"]),
    +    },
    +});
    +
    +const r = await sp.web();
    +
    +

    Calling Graph via MSAL

    +
    +

    When calling the graph API you must specify the scopes you need and ensure they are configured in AAD

    +
    +
    import { MsalClientSetup } from "@pnp/msaljsclient";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: MsalClientSetup({
    +            auth: {
    +                authority: "https://login.microsoftonline.com/tenant.onmicrosoft.com/",
    +                clientId: "00000000-0000-0000-0000-000000000000",
    +                redirectUri: "https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx",
    +            },
    +        }, ["Group.Read.All"]),
    +    },
    +});
    +
    +const r = await graph.groups();
    +
    +

    Use in Single Page Applications

    +

    You can also use the PnPjs MSAL client within your SPA applications. Please review the various settings to ensure you are configuring MSAL as needed for your application

    +
    import { MsalClientSetup } from "@pnp/msaljsclient";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: MsalClientSetup({
    +            auth: {
    +                authority: "https://login.microsoftonline.com/tenant.onmicrosoft.com/",
    +                clientId: "00000000-0000-0000-0000-000000000000",
    +                redirectUri: "https://myapp.com/login.aspx",
    +            },
    +        }, ["Group.Read.All"]),
    +    },
    +});
    +
    +const r = await graph.groups();
    +
    +

    Get a Token

    +

    You can also use the client to get a token if you need a token for use outside the PnPjs libraries

    +
    import { MsalClient } from "@pnp/msaljsclient";
    +
    +// note we do not provide scopes here as the second parameter. We certainly could and will get a token
    +// based on those scopes by making a call to getToken() without a param.
    +const client = new MsalClient({
    +    auth: {
    +        authority: "https://login.microsoftonline.com/{tenant}.onmicrosoft.com/",
    +        clientId: "00000000-0000-0000-0000-000000000000",
    +        redirectUri: "https://{tenant}.sharepoint.com/sites/dev/SitePages/webpacktest.aspx",
    +    },
    +});
    +
    +const token = await client.getToken(["Group.Read.All"]);
    +
    +const token2 = await client.getToken(["Files.Read"]);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/server-nodejs/index.html b/v2/authentication/server-nodejs/index.html new file mode 100644 index 000000000..0e000ec3f --- /dev/null +++ b/v2/authentication/server-nodejs/index.html @@ -0,0 +1,2404 @@ + + + + + + + + + + + + + + + + + + NodeJS Auth - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Authentication in Nodejs

    +

    SharePoint App Registration

    +
    +

    Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article. For testing we recommend using MSAL Certificate Auth.

    +
    +

    Within the PnPjs testing framework we make use of SharePoint App Registration. This uses the SPFetchClient client from the nodejs package. This client works based on the legacy SharePoint App Registration model making use of a client and secret granted permissions through AppInv.aspx. This method works and at the time of writing has no published end date.

    +

    See: details on how to register a legacy SharePoint application.

    +
    import { SPFetchClient } from "@pnp/nodejs";
    +import { sp } from "@pnp/sp/presets/all";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{site url}", "{client id}", "{client secret}");
    +        },
    +    },
    +});
    +
    +// execute a library request as normal
    +const w = await sp.web();
    +
    +

    MSAL

    +

    Added in 2.0.11

    +

    You can now use the @azure/msal-node client with PnPjs using MsalFetchClient. You must configure an AAD application with the appropriate permissions for your application.

    +
    +

    At the time this article was written the msal-node package is not yet GA.

    +
    +

    Call Graph

    +

    You can call the Microsoft Graph API with a client id and secret or certificate (see SharePoint example for cert auth)

    +
    import { graph } from "@pnp/graph/presets/all";
    +
    +// configure your node options
    +graph.setup({
    +  graph: {
    +    fetchClientFactory: () => {
    +      return new MsalFetchClient({
    +        auth: {
    +          authority: "https://login.microsoftonline.com/{tenant id or common}/",
    +          clientId: "{guid}",
    +          clientSecret: "{client secret}",
    +        }
    +      });
    +    },
    +  },
    +});
    +
    +const userInfo = await graph.users();
    +
    +

    Call SharePoint

    +

    To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below.

    +
    mkdir \temp
    +cd \temp
    +openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle'
    +openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass
    +
    +

    Using the above code you end up with three files, "cert.pem", "key.pem", and "keytmp.pem". The "cert.pem" file is uploaded to your AAD application registration. The "key.pem" is read as the private key for the configuration.

    +
    +

    You need to set the baseUrl property when using the MsalFetchClient

    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { readFileSync } from "fs";
    +
    +// read in our private key
    +const buffer = readFileSync("c:/temp/key.pem");
    +
    +// configure node options
    +sp.setup({
    +  sp: {
    +    baseUrl: "https://{my tenant}.sharepoint.com/sites/dev/",
    +    fetchClientFactory: () => {
    +      return new MsalFetchClient({
    +        auth: {
    +          authority: "https://login.microsoftonline.com/{tenant id or common}/",
    +          clientCertificate: {
    +            thumbprint: "{certificate thumbprint, displayed in AAD}",
    +            privateKey: buffer.toString(),
    +          },
    +          clientId: "{client id}",
    +        }
    +      }, ["https://{my tenant}.sharepoint.com/.default"]); // you must set the scope for SharePoint access
    +    },
    +  },
    +});
    +
    +const w = await sp.web();
    +
    +

    ADAL

    +

    The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected.

    +

    See: More details on the node client

    +
    import { AdalFetchClient } from "@pnp/nodejs";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +// setup the client using graph setup function
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +            return new AdalFetchClient("{tenant}", "{app id}", "{app secret}");
    +        },
    +    },
    +});
    +
    +// execute a library request as normal
    +const g = await graph.groups();
    +
    +console.log(JSON.stringify(g, null, 4));
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/authentication/sp-app-registration/index.html b/v2/authentication/sp-app-registration/index.html new file mode 100644 index 000000000..cbbb7139b --- /dev/null +++ b/v2/authentication/sp-app-registration/index.html @@ -0,0 +1,2273 @@ + + + + + + + + + + + + + + + + + + SP App Reg - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Legacy SharePoint App Registration

    +

    This section outlines how to register for a client id and secret for use in the above code.

    +
    +

    Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article. For testing we recommend using MSAL Certificate Authentication.

    +
    +

    Register An Add-In

    +

    Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly.

    +
      +
    1. Navigation to {site url}/_layouts/appregnew.aspx
    2. +
    3. Click "Generate" for both the Client Id and Secret values
    4. +
    5. Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions
    6. +
    7. Provide a fake value for app domain and redirect uri
    8. +
    9. Click "Create"
    10. +
    11. Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.
    12. +
    +

    Grant Your Add-In Permissions

    +

    Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site.

    +
      +
    1. Navigate to {admin site url}/_layouts/appinv.aspx
    2. +
    3. Paste your client id from the above section into the App Id box and click "Lookup"
    4. +
    5. You should see the information populated into the form from the last section, if not ensure you have the correct id value
    6. +
    7. Paste the below XML into the permissions request xml box and hit "Create"
    8. +
    9. You should get a confirmation message.
    10. +
    +
      <AppPermissionRequests AllowAppOnlyPolicy="true">
    +    <AppPermissionRequest Scope="http://sharepoint/content/tenant" Right="FullControl" />
    +    <AppPermissionRequest Scope="http://sharepoint/social/tenant" Right="FullControl" />
    +    <AppPermissionRequest Scope="http://sharepoint/search" Right="QueryAsUserIgnoreAppPrincipal" />
    +  </AppPermissionRequests>
    +
    +

    Note that the above XML will grant full tenant control. This is OK for testing, but you should grant only those permissions necessary for your application in production.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/collections/index.html b/v2/common/collections/index.html new file mode 100644 index 000000000..a3a883f1d --- /dev/null +++ b/v2/common/collections/index.html @@ -0,0 +1,2277 @@ + + + + + + + + + + + + + + + + + + collections - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/collections

    +

    The collections module provides typings and classes related to working with dictionaries.

    +

    TypedHash

    +

    Interface used to described an object with string keys corresponding to values of type T

    +
    export interface TypedHash<T> {
    +    [key: string]: T;
    +}
    +
    +

    objectToMap

    +

    Converts a plain object to a Map instance

    +
    const map = objectToMap({ a: "b", c: "d"});
    +
    +

    mergeMaps

    +

    Merges two or more maps, overwriting values with the same key. Last value in wins.

    +
    const m1 = new Map();
    +const m2 = new Map();
    +const m3 = new Map();
    +const m4 = new Map();
    +
    +const m = mergeMaps(m1, m2, m3, m4);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/custom-httpclientimpl/index.html b/v2/common/custom-httpclientimpl/index.html new file mode 100644 index 000000000..9b6f812be --- /dev/null +++ b/v2/common/custom-httpclientimpl/index.html @@ -0,0 +1,2300 @@ + + + + + + + + + + + + + + + + + + Custom HttpClientImpl - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Custom HttpClientImpl

    +

    This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation.

    +

    It is possible you may need complete control over the sending and receiving of requests.

    +

    Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation.

    +

    The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl.

    +
    export interface HttpClientImpl {
    +    fetch(url: string, options: FetchOptions): Promise<Response>;
    +}
    +
    +

    There is a single method "fetch" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface.

    +
    export interface FetchOptions {
    +    method?: string;
    +    headers?: HeadersInit | { [index: string]: string };
    +    body?: BodyInit;
    +    mode?: string | RequestMode;
    +    credentials?: string | RequestCredentials;
    +    cache?: string | RequestCache;
    +}
    +
    +

    So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read 👍.

    +

    Using Your Custom HttpClientImpl

    +

    Once you have written your implementation using it on your requests is done by setting it in the global library configuration:

    +
    import { setup } from "@pnp/core";
    +import { sp, Web } from "@pnp/sp";
    +import { MyAwesomeClient } from "./awesomeclient";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new MyAwesomeClient();
    +        }
    +    }
    +});
    +
    +let w = new Web("{site url}");
    +
    +// this request will use your client.
    +const result = await w.select("Title")();
    +console.log(result);
    +
    +

    Subclassing is Better

    +

    You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation.

    +

    A FINAL NOTE

    +

    Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/index.html b/v2/common/index.html new file mode 100644 index 000000000..cedf6573e --- /dev/null +++ b/v2/common/index.html @@ -0,0 +1,2262 @@ + + + + + + + + + + + + + + + + + + common - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core

    +

    npm version

    +

    The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well.

    +

    Getting Started

    +

    Install the library and required dependencies

    +

    npm install @pnp/core --save

    +

    Import and use functionality, see details on modules below.

    +
    import { getGUID } from "@pnp/core";
    +
    +console.log(getGUID());
    +
    +

    Exports

    + + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/libconfig/index.html b/v2/common/libconfig/index.html new file mode 100644 index 000000000..f6c3badc9 --- /dev/null +++ b/v2/common/libconfig/index.html @@ -0,0 +1,2413 @@ + + + + + + + + + + + + + + + + + + libconfig - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/libconfig

    +

    Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications.

    +

    ILibraryConfiguration Interface

    +

    Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below

    +
    export interface ILibraryConfiguration {
    +
    +    /**
    +     * Allows caching to be global disabled, default: false
    +     */
    +    globalCacheDisable?: boolean;
    +
    +    /**
    +     * Defines the default store used by the usingCaching method, default: session
    +     */
    +    defaultCachingStore?: "session" | "local";
    +
    +    /**
    +     * Defines the default timeout in seconds used by the usingCaching method, default 30
    +     */
    +    defaultCachingTimeoutSeconds?: number;
    +
    +    /**
    +     * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval
    +     */
    +    enableCacheExpiration?: boolean;
    +
    +    /**
    +     * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100)
    +     */
    +    cacheExpirationIntervalMilliseconds?: number;
    +
    +    /**
    +     * Used to supply the current context from an SPFx webpart to the library
    +     */
    +    spfxContext?: any;
    +}
    +
    +

    RuntimeConfigImpl

    +

    The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary +used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method.

    +

    assign

    +

    The assign method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below "Using RuntimeConfig within your application". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application.

    +
    import { RuntimeConfig } from "@pnp/core";
    +
    +// add your custom keys to the global configuration
    +// note you can use object hashes as values
    +RuntimeConfig.assign({
    +   "myKey1": "value 1",
    +   "myKey2": {
    +       "subKey": "sub value 1",
    +       "subKey2": "sub value 2",
    +   },
    +});
    +
    +// read your custom values
    +const v = RuntimeConfig.get("myKey1"); // "value 1"
    +
    +

    Using RuntimeConfig within your Application

    +

    If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties

    +
    import { ILibraryConfiguration, RuntimeConfig, ITypedHash } from "@pnp/core";
    +
    +// first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because
    +// TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions.
    +
    +// create the interface of your properties
    +// by creating this separately you allows others to compose your parts into their own config
    +interface MyConfigurationPart {
    +
    +    // you can create a grouped definition and access your settings as an object
    +    // keys can be optional or required as defined by your interface
    +    my?: {
    +        prop1?: string;
    +        prop2?: string;
    +    }
    +
    +    // and/or define multiple top level properties (beware key collision)
    +    // it is good practice to use a unique prefix
    +    myProp1: string;
    +    myProp2: number;
    +}
    +
    +// now create a combined interface
    +interface MyConfiguration extends ILibraryConfiguration, MyConfigurationPart { }
    +
    +
    +// now create a wrapper object and expose your properties
    +class MyRuntimeConfigImpl {
    +
    +    // exposing a nested property
    +    public get prop1(): ITypedHash<string> {
    +
    +        const myPart = RuntimeConfig.get("my");
    +        if (myPart !== null && typeof myPart !== "undefined" && typeof myPart.prop1 !== "undefined") {
    +            return myPart.prop1;
    +        }
    +
    +        return {};
    +    }
    +
    +    // exposing a root level property
    +    public get myProp1(): string | null {
    +
    +        let myProp1 = RuntimeConfig.get("myProp1");
    +
    +        if (myProp1 === null) {
    +            myProp1 = "some default value";
    +        }
    +
    +        return myProp1;
    +    }
    +
    +    setup(config: MyConfiguration): void {
    +        RuntimeConfig.assign(config);
    +    }
    +}
    +
    +// create a single static instance of your impl class
    +export let MyRuntimeConfig = new MyRuntimeConfigImpl();
    +
    +

    Now in other files you can use and set your configuration with a typed interface and properties

    +
    import { MyRuntimeConfig } from "{location of module}";
    +
    +
    +MyRuntimeConfig.setup({
    +    my: {
    +        prop1: "hello",
    +    },
    +});
    +
    +const value = MyRuntimeConfig.myProp1; // "hello"
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/netutil/index.html b/v2/common/netutil/index.html new file mode 100644 index 000000000..e44173460 --- /dev/null +++ b/v2/common/netutil/index.html @@ -0,0 +1,2350 @@ + + + + + + + + + + + + + + + + + + netutil - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/net

    +

    This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces +are described below (many have no use outside the library) as well as several classes.

    +

    Interfaces

    +

    HttpClientImpl

    +

    Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method "fetch" takes a URL and options. It returns a Promise<Response>. Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed.

    +

    RequestClient

    +

    An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The +difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the +underlying HttpClientImpl fetch method.

    +

    Classes

    +

    This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl.

    +

    FetchClient

    +

    Basic implementation that calls the global (window) fetch method with no additional processing.

    +
    import { FetchClient } from "@pnp/core";
    +
    +const client = new FetchClient();
    +
    +client.fetch("{url}", {});
    +
    +

    BearerTokenFetchClient

    +

    A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string.

    +
    import { BearerTokenFetchClient } from "@pnp/core";
    +
    +const client = new BearerTokenFetchClient("{authentication token}");
    +
    +client.fetch("{url}", {});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/storage/index.html b/v2/common/storage/index.html new file mode 100644 index 000000000..0eecaf8c7 --- /dev/null +++ b/v2/common/storage/index.html @@ -0,0 +1,2339 @@ + + + + + + + + + + + + + + + + + + storage - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/storage

    +

    This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.

    +

    PnPClientStorage

    +

    The main export of this module, contains properties representing local and session storage.

    +
    import { PnPClientStorage } from "@pnp/core";
    +
    +const storage = new PnPClientStorage();
    +const myvalue = storage.local.get("mykey");
    +
    +

    PnPClientStorageWrapper

    +

    Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used +from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage.

    +
    import { PnPClientStorage } from "@pnp/core";
    +
    +const storage = new PnPClientStorage();
    +
    +// get a value from storage
    +const value = storage.local.get("mykey");
    +
    +// put a value into storage
    +storage.local.put("mykey2", "my value");
    +
    +// put a value into storage with an expiration
    +storage.local.put("mykey2", "my value", new Date());
    +
    +// put a simple object into storage
    +// because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects
    +storage.local.put("mykey3", {
    +    key: "value",
    +    key2: "value2",
    +});
    +
    +// remove a value from storage
    +storage.local.delete("mykey3");
    +
    +// get an item or add it if it does not exist
    +// returns a promise in case you need time to get the value for storage
    +// optionally takes a third parameter specifying the expiration
    +storage.local.getOrPut("mykey4", () => {
    +    return Promise.resolve("value");
    +});
    +
    +// delete expired items
    +storage.local.deleteExpired();
    +
    +

    Cache Expiration

    +

    The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient.

    +
    import { PnPClientStorage } from "@pnp/core";
    +
    +const storage = new PnPClientStorage();
    +
    +// session storage
    +storage.session.deleteExpired();
    +
    +// local storage
    +storage.local.deleteExpired();
    +
    +// this returns a promise, so you can perform some activity after the expired items are removed:
    +storage.local.deleteExpired().then(_ => {
    +    // init my application
    +});
    +
    +

    The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout.

    +
    
    +import { setup } from "@pnp/core";
    +
    +setup({
    +    enableCacheExpiration: true,
    +    cacheExpirationIntervalMilliseconds: 1000, // optional
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/common/util/index.html b/v2/common/util/index.html new file mode 100644 index 000000000..b2d3409c8 --- /dev/null +++ b/v2/common/util/index.html @@ -0,0 +1,2638 @@ + + + + + + + + + + + + + + + + + + util - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/core/util

    +

    This module contains utility methods that you can import individually from the common library.

    +
    import {
    +    getRandomString,
    +} from "@pnp/core";
    +
    +// use from individually imported method
    +console.log(getRandomString(10));
    +
    +

    assign

    +

    Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing +properties.

    +
    import { assign } from "@pnp/core";
    +
    +let obj1 = {
    +    prop: 1,
    +    prop2: 2,
    +};
    +
    +const obj2 = {
    +    prop: 4,
    +    prop3: 9,
    +};
    +
    +const example1 = assign(obj1, obj2);
    +// example1 = { prop: 4, prop2: 2, prop3: 9 }
    +
    +//noOverwrite = true stops overwriting existing properties
    +const example2 = assign(obj1, obj2, true);
    +// example2 = { prop: 1, prop2: 2, prop3: 9 }
    +
    +

    combine

    +

    Combines any number of paths, normalizing the slashes as required

    +
    import { combine } from "@pnp/core";
    +
    +// "https://microsoft.com/something/more"
    +const paths = combine("https://microsoft.com", "something", "more");
    +
    +// "also/works/with/relative"
    +const paths2 = combine("/also/", "/works", "with/", "/relative\\");
    +
    +

    dateAdd

    +

    Manipulates a date, please see the Stack Overflow discussion from where this method was taken.

    +
    import { dateAdd } from "@pnp/core";
    +
    +const testDate = new Date();
    +
    +dateAdd(testDate,'minute',10);
    +
    +

    getCtxCallback

    +

    Gets a callback function which will maintain context across async calls.

    +
    import { getCtxCallback } from "@pnp/core";
    +
    +const contextThis = {
    +    myProp: 6,
    +};
    +
    +function theFunction() {
    +    // "this" within this function will be the context object supplied
    +    // in this case the variable contextThis, so myProp will exist
    +    return this.myProp;
    +}
    +
    +const callback = getCtxCallback(contextThis, theFunction);
    +
    +callback(); // returns 6
    +
    +// You can also supply additional parameters if needed
    +
    +function theFunction2(g: number) {
    +    // "this" within this function will be the context object supplied
    +    // in this case the variable contextThis, so myProp will exist
    +    return this.myProp + g;
    +}
    +
    +const callback2 = getCtxCallback(contextThis, theFunction2, 4);
    +
    +callback2(); // returns 10 (6 + 4)
    +
    +

    getGUID

    +

    Creates a random guid, please see the Stack Overflow discussion from where this method was taken.

    +
    import { getGUID } from "@pnp/core";
    +
    +const newGUID = getGUID();
    +
    +

    getRandomString

    +

    Gets a random string consisting of the number of characters requested.

    +
    import { getRandomString } from "@pnp/core";
    +
    +const randomString = getRandomString(10);
    +
    +

    hOP

    +

    Shortcut for Object.hasOwnProperty. Determines if an object has a specified property.

    +
    import { HttpRequestError } from "@pnp/queryable";
    +import { hOP } from "@pnp/core";
    +
    +export async function handleError(e: Error | HttpRequestError): Promise<void> {
    +
    +  //Checks to see if the error object has a property called isHttpRequestError. Returns a bool.
    +  if (hOP(e, "isHttpRequestError")) {
    +      // Handle this type or error
    +  } else {
    +    // not an HttpRequestError so we do something else
    +
    +  }
    +}
    +
    +

    isArray

    +

    Determines if a supplied variable represents an array.

    +
    import { isArray } from "@pnp/core";
    +
    +let x:String[] = [1,2,3]];
    +
    +if (isArray(x)){
    +    console.log("I am an array");
    +}else{
    +    console.log("I am not an array");
    +}
    +
    +

    isFunc

    +

    Determines if a supplied variable represents a function.

    +
    import { isFunc } from "@pnp/core";
    +
    +public testFunction() {
    +    console.log("test function");
    +    return
    +}
    +
    +if (isFunc(testFunction)){
    +    console.log("this is a function");
    +    testFunction();
    +}
    +
    +

    isUrlAbsolute

    +

    Determines if a supplied url is absolute and returns true; otherwise returns false.

    +
    import { isUrlAbsolute } from "@pnp/core";
    +
    +const webPath = 'https://{tenant}.sharepoint.com/sites/dev/';
    +
    +if (isUrlAbsolute(webPath)){
    +    console.log("URL is absolute");
    +}else{
    +    console.log("URL is not absolute");
    +}
    +
    +

    objectDefinedNotNull

    +

    Determines if an object is defined and not null.

    +
    import { objectDefinedNotNull } from "@pnp/core";
    +
    +let obj = {
    +    prop: 1
    +};
    +
    +if (objectDefinedNotNull(obj)){
    +    console.log("Not null");
    +}else{
    +    console.log("Null");
    +}
    +
    +

    stringIsNullOrEmpty

    +

    Determines if a supplied string is null or empty.

    +
    import { stringIsNullOrEmpty } from "@pnp/core";
    +
    +let x:String = "hello";
    +
    +if (stringIsNullOrEmpty(x)){
    +    console.log("Null or empty");
    +}else{
    +    console.log("Not null or empty");
    +}
    +
    +

    Removed

    +

    Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods +below for use in your projects should you require.

    +
    /**
    + * Loads a stylesheet into the current page
    + *
    + * @param path The url to the stylesheet
    + * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues
    + */
    +public static loadStylesheet(path: string, avoidCache: boolean): void {
    +    if (avoidCache) {
    +        path += "?" + encodeURIComponent((new Date()).getTime().toString());
    +    }
    +    const head = document.getElementsByTagName("head");
    +    if (head.length > 0) {
    +        const e = document.createElement("link");
    +        head[0].appendChild(e);
    +        e.setAttribute("type", "text/css");
    +        e.setAttribute("rel", "stylesheet");
    +        e.setAttribute("href", path);
    +    }
    +}
    +
    +/**
    + * Tests if a url param exists
    + *
    + * @param name The name of the url parameter to check
    + */
    +public static urlParamExists(name: string): boolean {
    +    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    +    const regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
    +    return regex.test(location.search);
    +}
    +
    +/**
    + * Gets a url param value by name
    + *
    + * @param name The name of the parameter for which we want the value
    + */
    +public static getUrlParamByName(name: string): string {
    +    name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
    +    const regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
    +    const results = regex.exec(location.search);
    +    return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
    +}
    +
    +/**
    + * Gets a url param by name and attempts to parse a bool value
    + *
    + * @param name The name of the parameter for which we want the boolean value
    + */
    +public static getUrlParamBoolByName(name: string): boolean {
    +    const p = this.getUrlParamByName(name);
    +    const isFalse = (p === "" || /false|0/i.test(p));
    +    return !isFalse;
    +}
    +
    +/**
    + * Inserts the string s into the string target as the index specified by index
    + *
    + * @param target The string into which we will insert s
    + * @param index The location in target to insert s (zero based)
    + * @param s The string to insert into target at position index
    + */
    +public static stringInsert(target: string, index: number, s: string): string {
    +    if (index > 0) {
    +        return target.substring(0, index) + s + target.substring(index, target.length);
    +    }
    +    return s + target;
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/configuration/index.html b/v2/concepts/configuration/index.html new file mode 100644 index 000000000..db47642f8 --- /dev/null +++ b/v2/concepts/configuration/index.html @@ -0,0 +1,2685 @@ + + + + + + + + + + + + + + + + + + Configuration - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    PnPjs Configuration

    +

    This article describes the configuration architecture used by the library as well as the settings available.

    +
    +

    Starting with version 2.1.0 we updated our configuration design to support the ability to isolate settings to individual objects. The first part of this article discusses the newer design, you can read about the pre v2.1.0 configuration further down.

    +
    +

    Post v2.1.0

    +

    Architecture

    +

    Starting from v2.1.0 we have modified our configuration design to allow for configuring individual queryable objects.

    +

    Backward Compatibility

    +

    If you have no need to use the isolated runtimes introduced in 2.1.0 then you should see no change in library behavior from prior versions. You can continue to refer to the pre v2.1.0 configuration section - and if you see any issues please let us know.

    +

    All of the available settings as described below remain, unchanged.

    +
    +

    If you previously used our internal configuration classes directly RuntimeConfigImpl, SPRuntimeConfigImpl, or GraphRuntimeConfigImpl they no longer exist. We do not consider this a breaking change as they were meant to be internal and their direct use was not documented. This includes the concrete default instances RuntimeConfig, SPRuntimeConfig, and GraphRuntimeConfig.

    +
    +

    Isolated Runtimes

    +

    You can create an isolated runtime when using either the sp or graph libraries. What this does is create an isolated set of properties and behaviors specific to a given fluent chain. Have a look at this basic example below:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// create an isolated sp root instance
    +const isolatedSP = await sp.createIsolated();
    +
    +// this configuration applies to all objects created from "sp"
    +sp.setup({
    +  sp: {
    +    baseUrl: "https://mytenant.sharepoint.com/",
    +  },
    +});
    +
    +// this configuration applies to all objects created from "isolatedSP"
    +isolatedSP.setup({
    +  sp: {
    +    baseUrl: "https://mytenant.sharepoint.com/sites/dev",
    +  },
    +});
    +
    +// details for the web at https://mytenant.sharepoint.com/
    +const web1 = await sp.web();
    +
    +// details for the web at https://mytenant.sharepoint.com/sites/dev
    +const web2 = await isolatedSP.web();
    +
    +

    This configuration is supplied to all objects down a given fluent chain:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +
    +// create an isolated sp root instance
    +const isolatedSP = await sp.createIsolated();
    +
    +// this configuraiton applies to all objects created from "sp"
    +sp.setup({
    +  sp: {
    +    baseUrl: "https://mytenant.sharepoint.com/",
    +  },
    +});
    +
    +// this configuraiton applies to all objects created from "isolatedSP"
    +isolatedSP.setup({
    +  sp: {
    +    baseUrl: "https://mytenant.sharepoint.com/sites/dev",
    +  },
    +});
    +
    +// details for the lists at https://mytenant.sharepoint.com/
    +const lists1 = await sp.web.lists();
    +
    +// details for the lists at https://mytenant.sharepoint.com/sites/dev
    +const lists2 = await isolatedSP.web.lists();
    +
    +

    createIsolated

    +

    The createIsolated method is used to establish the isolated runtime for a given instance of either the sp or graph libraries. Once created it is no longer connected to the default instance and if you have common settings that must be updated you would need to update them across each isolated instance, this is by design. Currently sp and graph createIsolated methods accept the same init, but we have broken them out to make thing clear. All properties of the init object are optional. Any properties provided will overwrite those cloned from the default if cloneGlobal is true. If cloneGlobal is false you start with an empty config containing only the core defaults.

    +

    sp.createIsolated

    +
    import { sp, ISPConfiguration } from "@pnp/sp";
    +
    +// accept all the defaults, will clone any settings from sp
    +const isolatedSP = await sp.createIsolated();
    +
    +// - specify all the config options, using the ISPConfiguration interface to type the config
    +// - setting baseUrl in the root is equivelent to setting it with sp: { baseUrl: }, it is provided as a shortcut as this seemed to be a common use case
    +//   - if you set them both the baseUrl in the root will be used.
    +// - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values
    +//   - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below
    +const isolatedSP = await sp.createIsolated<ISPConfiguration>({
    +  baseUrl: "https://mytenant.sharepoint.com",
    +  cloneGlobal: false,
    +  config: {
    +    cacheExpirationIntervalMilliseconds: 1000,
    +    sp: {
    +      baseUrl: "https://mytenant.sharepoint.com",
    +      fetchClientFactory: () => void(0),
    +      headers: {
    +        "X-AnotherHeader": "54321",
    +      },
    +    },
    +    spfxContext: this.context, // only valid within SPFx
    +  },
    +  options: {
    +    headers: {
    +      "X-SomeHeader": "12345",
    +    },
    +  },
    +});
    +
    +

    Defaults

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDefault
    baseUrl""
    cloneGlobaltrue
    config{}
    options{}
    +

    graph.createIsolated

    +
    import { graph, IGraphConfiguration } from "@pnp/graph";
    +
    +// - specify all the config options, using the IGraphConfiguration interface to type the config
    +// - setting baseUrl in the root is restricted to "v1.0" or "beta". If you need to specify a different absolute url should use config.graph.baseUrl
    +//   - in practice you should use one or the other. You can always swap Graph api version using IGraphQueryable.setEndpoint
    +// - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values
    +//   - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below
    +const isolatedGraph = await graph.createIsolated<IGraphConfiguration>({
    +  baseUrl: "v1.0",
    +  cloneGlobal: false,
    +  config: {
    +    cacheExpirationIntervalMilliseconds: 1000,
    +    graph: {
    +      baseUrl: "https://graph.microsoft.com",
    +      fetchClientFactory: () => void(0),
    +      headers: {
    +        "X-AnotherHeader": "54321",
    +      },
    +    },
    +    spfxContext: this.context, // only valid within SPFx
    +  },
    +  options: {
    +    headers: {
    +      "X-SomeHeader": "12345",
    +    },
    +  },
    +});
    +
    +

    Defaults

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    nameDefault
    baseUrl"v1.0"
    cloneGlobaltrue
    config{}
    options{}
    +

    Additional Examples

    +

    MSAL with Node multiple site requests

    +

    MSAL Support Added in 2.0.11

    +

    In this example you can see how you can setup the MSAL client once and then set a different baseUrl for an isolated instance. More information specific to setting up the MSAL client is available.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { readFileSync } from "fs";
    +
    +// read in our private key
    +const buffer = readFileSync("c:/temp/key.pem");
    +
    +// configure node options
    +sp.setup({
    +  sp: {
    +    baseUrl: "https://{my tenant}.sharepoint.com/sites/dev/",
    +    fetchClientFactory: () => {
    +      return new MsalFetchClient({
    +        auth: {
    +          authority: "https://login.microsoftonline.com/{tenant id or common}",
    +          clientCertificate: {
    +            thumbprint: "{certificate thumbprint, displayed in AAD}",
    +            privateKey: buffer.toString(),
    +          },
    +          clientId: "{client id}",
    +        }
    +      }, ["https://{my tenant}.sharepoint.com/.default"]); // you must set the scope for SharePoint access
    +    },
    +  },
    +});
    +
    +const isolatedSP = await sp.createIsolated<ISPConfigurationPart>({
    +  config: {
    +    sp: {
    +      baseUrl: "https://{my tenant}.sharepoint.com/sites/dev2/",
    +    },
    +  },
    +});
    +
    +

    Node multiple site requests

    +

    Isolated configuration was most requested for scenarios in node where you need to access information in multiple sites. This example shows setting up the global configuration and then creating an isolated config with only the baseUrl updated.

    +
    import { SPFetchClient } from "@pnp/nodejs";
    +import { ISPConfigurationPart, sp } from "@pnp/sp";
    +
    +sp.setup({
    +  cacheExpirationIntervalMilliseconds: 1000,
    +  defaultCachingStore: "local",
    +  sp: {
    +    fetchClientFactory: () => {
    +      return new SPFetchClient("https://mytenant.sharepoint.com/", "id", "secret");
    +    },
    +    headers: {
    +      "X-MyRequiredHeader": "SomeValue",
    +      "X-MyRequiredHeader2": "SomeValue",
    +    },
    +  },
    +});
    +
    +const isolatedSP = await sp.createIsolated<ISPConfigurationPart>({
    +  config: {
    +    sp: {
    +      fetchClientFactory: () => {
    +        return new SPFetchClient("https://mytenant.sharepoint.com/site/dev", "id", "secret");
    +      },
    +    },
    +  },
    +});
    +
    +

    Batching

    +

    All batching functionality works as expected, but you must take care to only associate requests from the same isolated instance as you create the batch. Mixing requests across isolation boundaries is not supported. This applies to sp and graph batching.

    +
    sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("url1", "id", "secret");
    +        },
    +    },
    +});
    +
    +const isolated = await sp.createIsolated<ISPConfiguration>({
    +    config: {
    +        sp: {
    +            fetchClientFactory: () => {
    +                return new SPFetchClient("url2", "id", "secret");
    +            },
    +        },
    +    },
    +});
    +
    +const batch1 = sp.createBatch();
    +sp.web.lists.select("Title").top(3).inBatch(batch1)().then(r => console.log(`here 1: ${JSON.stringify(r, null, 2)}`));
    +sp.web.select("Title").inBatch(batch1)().then(r => console.log(`here 2: ${JSON.stringify(r, null, 2)}`));
    +await batch1.execute();
    +
    +const batch2 = isolated.createBatch();
    +isolated.web.lists.select("Title").top(3).inBatch(batch2)().then(r => console.log(`here 3: ${JSON.stringify(r, null, 2)}`));
    +isolated.web.select("Title").inBatch(batch2)().then(r => console.log(`here 4: ${JSON.stringify(r, null, 2)}`));
    +await batch2.execute();
    +
    +

    IE11 Mode

    +

    The IE11 mode setting is always global. There is no scenario we care to support where once instance needs to run in ie11 mode and another does not. Your code either does or does not run in ie11.

    +

    Prior to v2.1.0

    +

    Architecture

    +

    PnPjs uses an additive configuration design with multiple libraries sharing a single global configuration instance. If you need non-global configuration please see this section. There are three ways to access the setup functionality - through either the common, sp, or graph library's setup method. While the configuration is global the various methods have different typing on their input parameter. You can review the libconfig article for more details on storing your own configuration.

    +

    Common Configuration

    +

    The common libary's setup method takes parameters defined by ILibraryConfiguration. The properties and their defaults are listed below, followed by a code sample. You can call setup multiple times and any new values will be added to the existing configuration or replace the previous value if one existed.

    +

    All values are optional.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionDefault
    defaultCachingStoreWhere will PnPjs store cached data by default (session or local)session
    defaultCachingTimeoutSecondsThe global default value used for cached data timeouts in seconds60
    globalCacheDisableProvides a way to globally within PnPjs disable all cachingfalse
    enableCacheExpirationIf true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutIntervalfalse
    cacheExpirationIntervalMillisecondsDetermines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100)750
    spfxContextWhen running in SPFx the current context should always be supplied to PnPjs when availablenull
    ie11If true the library downgrades functionality to work in IE11false
    +
    +

    For more information on setting up in SPFx please see the authentication section

    +

    For more details on ie11 mode please see the topic article

    +
    +
    import { setup } from "@pnp/core";
    +
    +// called before other code
    +setup({
    +  cacheExpirationIntervalMilliseconds: 15000,
    +  defaultCachingStore: "local",
    +  defaultCachingTimeoutSeconds: 600,
    +  enableCacheExpiration: true,
    +  globalCacheDisable: false,
    +  ie11: false,
    +  spfxContext: this.context, // if in SPFx, otherwise leave it out
    +});
    +
    +

    SP Configuration

    +

    The sp library's configuration is defined by the ISPConfiguration interface which extends ILibraryConfiguration. All of the sp values are contained in a top level property named "sp". The following table describes the properties with a code sample following.

    +

    All values are optional.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionDefault
    headersAllows you to apply any headers to all calls made by the sp librarynone
    baseUrlAllows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute.none
    fetchClientFactoryAllows you to specify a factory function used to produce IHttpClientImpl instancesnone
    +
    +

    There are many examples of using fetchClientFactory available in the authentication section.

    +
    +
    import { sp } from "@pnp/sp";
    +import { SPFxAdalClient } from "@pnp/core";
    +
    +// note you can still set the global configuration such as ie11 using the same object as 
    +// the interface extends ILibraryConfiguration
    +sp.setup({
    +  ie11: false,
    +  sp: {
    +    baseUrl: "https://tenant.sharepoint.com/sites/dev",
    +    fetchClientFactory: () => {
    +      return new SPFxAdalClient(this.context);
    +    },
    +    headers: {
    +      "Accept": "application/json;odata=verbose",
    +      "X-Something": "header-value",
    +    },
    +  },
    +  spfxContext: this.context,
    +});
    +
    +

    SharePoint Framework

    +

    You can optionally supply only the SPFx context to the sp configure method.

    +
    import { sp } from "@pnp/sp";
    +
    +// in SPFx only
    +sp.setup(this.context);
    +
    +

    Graph Configuration

    +

    The graph configuration works exactly the same as the sp configuration but is defined by the IGraphConfiguration interface which extends ILibraryConfiguration. All of the graph values are contained in a top level property named "graph". The following table describes the properties with a code sample following.

    +

    All values are optional.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameDescriptionDefault
    headersAllows you to apply any headers to all calls made by the sp librarynone
    baseUrlAllows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. (Added in 2.0.8)none
    fetchClientFactoryAllows you to specify a factory function used to produce IHttpClientImpl instancesnone
    +
    +

    There are many examples of using fetchClientFactory available in the authentication section.

    +
    +
    import { graph } from "@pnp/graph";
    +import { MsalClientSetup } from "@pnp/msaljsclient";
    +
    +// note you can still set the global configuration such as ie11 using the same object as 
    +// the interface extends ILibraryConfiguration
    +graph.setup({
    +  ie11: false,
    +  graph: {
    +    // we set the GCC url
    +    baseUrl: "https://graph.microsoft.us",
    +    fetchClientFactory: MsalClientSetup({
    +        auth: {
    +            authority: "https://login.microsoftonline.com/tenant.onmicrosoft.com",
    +            clientId: "00000000-0000-0000-0000-000000000000",
    +            redirectUri: "https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx",
    +        },
    +    }, ["Group.Read.All"]),
    +    headers: {
    +      "Accept": "application/json;odata=verbose",
    +      "X-Something": "header-value",
    +    },
    +  },
    +  spfxContext: this.context,
    +});
    +
    +

    SharePoint Framework

    +

    You can optionally supply only the SPFx context to the graph configure method. We will attempt to set the baseUrl property from the context - but if that is failing in your environment and you need to call a special cloud (i.e. graph.microsoft.us) please set the baseUrl property.

    +
    import { graph } from "@pnp/graph";
    +
    +// in SPFx only
    +graph.setup(this.context);
    +
    +

    Configure Everything At Once

    +

    In some cases you might want to configure everything in one go. Because the configuration is stored in a single location you can use the common library's setup method and adjust the typings to ensure you are using the correct property names while only having to setup things with a single call.

    +
    +

    In versions before 2.0.8 ISPConfigurationPart, IGraphConfigurationPart, and ILibraryConfiguration incorrectly were missing the "I" prefix. That was fixed in 2.0.8 - but note if you are using an older version of the library you'll need to use the old names. Everything else in the below example works as expected.

    +
    +
    import { ISPConfigurationPart } from "@pnp/sp";
    +import { IGraphConfigurationPart } from "@pnp/graph";
    +import { ILibraryConfiguration, setup } from "@pnp/core";
    +
    +// you could also include your custom configuration parts
    +export interface AllConfig extends ILibraryConfiguration, ISPConfigurationPart, IGraphConfigurationPart { }
    +
    +// create a single big configuration entry
    +const config: AllConfig = {
    +  graph: {
    +    baseUrl: "https://graph.microsoft.us",
    +  },
    +  ie11: false,
    +  sp: {
    +    baseUrl: "https://tenant.sharepoint.com/sites/dev",
    +  },
    +};
    +
    +setup(config);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/custom-bundle/index.html b/v2/concepts/custom-bundle/index.html new file mode 100644 index 000000000..375bc30c8 --- /dev/null +++ b/v2/concepts/custom-bundle/index.html @@ -0,0 +1,2279 @@ + + + + + + + + + + + + + + + + + + Custom Bundle - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Custom Bundling

    +

    With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles.

    +

    Scenarios could include:

    +
      +
    • Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once.
    • +
    • Creating SPFx libraries either for one project or a single webpart.
    • +
    • Create a single library containing the PnPjs code you need bundled along with your custom extensions.
    • +
    +

    Create a custom bundle

    +

    Webpack

    +

    You can see/clone a sample project of this example here.

    +

    Rollup

    +

    You can see/clone a sample project of this example here.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/error-handling/index.html b/v2/concepts/error-handling/index.html new file mode 100644 index 000000000..ad8111303 --- /dev/null +++ b/v2/concepts/error-handling/index.html @@ -0,0 +1,2539 @@ + + + + + + + + + + + + + + + + + + Error Handling - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Error Handling

    +

    This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns.

    +
    +

    For 429, 503, and 504 errors we include retry logic within the library

    +
    +

    The HttpRequestError

    +

    All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error. In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Property NameDescription
    nameStandard Error.name property. Always 'Error'
    messageNormalized string containing the status, status text, and the full response text
    stackThe callstack producing the error
    isHttpRequestErrorAlways true, allows you to reliably determine if you have an HttpRequestError instance
    responseUnread copy of the Response object resulting in the thrown error
    statusThe Response.status value (such as 404)
    statusTextThe Response.statusText value (such as 'Not Found')
    +

    Basic Handling

    +

    For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen 😉. The most basic type of error handling involves a simple try-catch.

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +try {
    +
    +  // get a list that doesn't exist
    +  const w = await sp.web.lists.getByTitle("no")();
    +
    +} catch (e) {
    +
    +  console.error(e);
    +}
    +
    +

    This will produce output like:

    +
    Error making HttpClient request in queryable [404] Not Found ::> {"odata.error":{"code":"-1, System.ArgumentException","message":{"lang":"en-US","value":"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'."}}} Data: {"response":{"size":0,"timeout":0},"status":404,"statusText":"Not Found","isHttpRequestError":true}
    +
    +

    This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.

    +

    Reading the Response

    +

    In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire:

    +
    import { sp } from "@pnp/sp/presets/all";
    +import { HttpRequestError } from "@pnp/queryable";
    +
    +try {
    +
    +  // get a list that doesn't exist
    +  const w = await sp.web.lists.getByTitle("no")();
    +
    +} catch (e) {
    +
    +  // are we dealing with an HttpRequestError?
    +  if (e?.isHttpRequestError) {
    +
    +    // we can read the json from the response
    +    const json = await (<HttpRequestError>e).response.json();
    +
    +    // if we have a value property we can show it
    +    console.log(typeof json["odata.error"] === "object" ? json["odata.error"].message.value : e.message);
    +
    +    // add of course you have access to the other properties and can make choices on how to act
    +    if ((<HttpRequestError>e).status === 404) {
    +       console.error((<HttpRequestError>e).statusText);
    +      // maybe create the resource, or redirect, or fallback to a secondary data source
    +      // just ideas, handle any of the status codes uniquely as needed
    +    }
    +
    +  } else {
    +    // not an HttpRequestError so we just log message
    +    console.log(e.message);
    +  }
    +}
    +
    +

    Logging errors

    +

    Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework.

    +
    import { Logger } from "@pnp/logging";
    +import { sp } from "@pnp/sp/presets/all";
    +
    +try {
    +  // get a list that doesn't exist
    +  const w = await sp.web.lists.getByTitle("no")();  
    +} catch (e) {
    +
    +  Logger.error(e);
    +}
    +
    +

    You may want to read the response and customize the message as described above:

    +
    import { Logger } from "@pnp/logging";
    +import { sp } from "@pnp/sp/presets/all";
    +import { HttpRequestError } from "@pnp/queryable";
    +
    +try {
    +  // get a list that doesn't exist
    +  const w = await sp.web.lists.getByTitle("no")();  
    +} catch (e) {
    +
    +  if (e?.isHttpRequestError) {
    +
    +    // we can read the json from the response
    +    const data = await (<HttpRequestError>e).response.json();
    +
    +    // parse this however you want
    +    const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;
    +
    +    // we use the status to determine a custom logging level
    +    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;
    +
    +    // create a custom log entry
    +    Logger.log({
    +      data,
    +      level,
    +      message,
    +    });
    +
    +  } else {
    +    // not an HttpRequestError so we just log message
    +    Logger.error(e);
    +  }
    +}
    +
    +

    Putting it All Together

    +

    After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.

    +

    errorhandler.ts

    +
    import { Logger } from "@pnp/logging";
    +import { HttpRequestError } from "@pnp/queryable";
    +import { hOP } from "@pnp/core";
    +
    +export async function handleError(e: Error | HttpRequestError): Promise<void> {
    +
    +  if (hOP(e, "isHttpRequestError")) {
    +
    +    // we can read the json from the response
    +    const data = await (<HttpRequestError>e).response.json();
    +
    +    // parse this however you want
    +    const message = typeof data["odata.error"] === "object" ? data["odata.error"].message.value : e.message;
    +
    +    // we use the status to determine a custom logging level
    +    const level: LogLevel = (<HttpRequestError>e).status === 404 ? LogLevel.Warning : LogLevel.Info;
    +
    +    // create a custom log entry
    +    Logger.log({
    +      data,
    +      level,
    +      message,
    +    });
    +
    +  } else {
    +    // not an HttpRequestError so we just log message
    +    Logger.error(e);
    +  }
    +}
    +
    +

    web-request.ts

    +
    import { sp } from "@pnp/sp/presets/all";
    +import { handleError } from "./errorhandler";
    +
    +try {
    +
    +  const w = await sp.web.lists.getByTitle("no")();
    +
    +} catch (e) {
    +
    +  await handleError(e);
    +}
    +
    +

    web-request2.ts

    +
    import { sp } from "@pnp/sp/presets/all";
    +import { handleError } from "./errorhandler";
    +
    +try {
    +
    +  const w = await sp.web.lists();
    +
    +} catch (e) {
    +
    +  await handleError(e);
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/ie11-mode/index.html b/v2/concepts/ie11-mode/index.html new file mode 100644 index 000000000..a8d02830a --- /dev/null +++ b/v2/concepts/ie11-mode/index.html @@ -0,0 +1,2278 @@ + + + + + + + + + + + + + + + + + + IE11 Mode - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    IE11 Mode

    +

    Starting with v2 we have made the decision to no longer support IE11. Because we know this affects folks we have introduced IE11 compatibility mode. Configuring the library will allow it to work within IE11, however at a possibly reduced level of functionality depending on your use case. Please see the list below of known limitations.

    +

    Limitations

    +

    When required to use IE11 mode there is certain functionality which may not work correctly or at all.

    + +

    Configure IE11 Mode

    +

    To enable IE11 Mode set the ie11 flag to true in the setup object. Optionally, supply the context object when working in SharePoint Framework.

    +
    import { sp } from "@pnp/sp";
    +
    +sp.setup({
    +  // set ie 11 mode
    +  ie11: true,
    +  // only needed when working within SharePoint Framework
    +  spfxContext: this.context
    +});
    +
    +
    +

    If you are supporting IE 11, please see the article on required polyfills.

    +
    +

    A note on ie11 mode and support

    +

    Because IE11 is no longer a primary supported browser our policy moving forward will be doing our best not to break anything in ie11 mode, but not all features will work and new features may never come to ie11 mode. Also, if you find an ie11 bug we expect you to work with us on helping to fix it. If you aren't willing to invest some time to support an old browser it seems we shouldn't either.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/invokable/index.html b/v2/concepts/invokable/index.html new file mode 100644 index 000000000..7ca6d66a1 --- /dev/null +++ b/v2/concepts/invokable/index.html @@ -0,0 +1,2254 @@ + + + + + + + + + + + + + + + + + + Invokables - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Invokables

    +

    For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain:

    +
    // an example of get
    +const lists = await sp.web.lists();
    +
    +

    Starting with v2 this is no longer required, you can invoke the object directly to execute the default action for that class - typically a get.

    +
    const lists = await sp.web.lists();
    +
    +

    This has two main benefits for people using the library: you can write less code, and we now have a way to model default actions for objects that might do something other than a get. The way we designed the library prior to v2 hid the post, put, delete operations as protected methods attached to the Queryable classes. Without diving into why we did this, having a rethink seemed appropriate for v2. Based on that, the entire queryable chain is now invokable as well for any of the operations.

    +

    Other Operations (post, put, delete)

    +
    import { sp, spPost } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// do a post to a web - just an example doesn't do anything fancy
    +spPost(sp.web);
    +
    +

    Things get a little more interesting in that you can now do posts (or any of the operations) to any of the urls defined by a fluent chain. Meaning you can easily implement methods that are not yet part of the library. For this example I have made up a method called "MagicFieldCreationMethod" that doesn't exist. Imagine it was just added to the SharePoint API and we do not yet have support for it. You can now write code like so:

    +
    import { sp, spPost, SharePointQueryable } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/fields/web";
    +
    +// call our made up example method
    +spPost(SharePointQueryable(sp.web.fields, "MagicFieldCreationMethod"), {
    +    body: JSON.stringify({
    +        // ... this would be the post body
    +    }),
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/polyfill/index.html b/v2/concepts/polyfill/index.html new file mode 100644 index 000000000..5a7977162 --- /dev/null +++ b/v2/concepts/polyfill/index.html @@ -0,0 +1,2401 @@ + + + + + + + + + + + + + + + + + + Polyfills - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Polyfills

    +

    These libraries may make use of some features not found in older browsers. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality.

    +
    +

    If you are supporting IE11 enable IE11 mode.

    +
    +

    IE 11 Polyfill package

    +

    We created a package you try and help provide this missing functionality. This package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you are required to support IE 11.

    +

    Install

    +

    npm install @pnp/polyfill-ie11 --save

    +

    Use

    +
    import "@pnp/polyfill-ie11";
    +import { sp } from "@pnp/sp/presets/all";
    +
    +sp.web.lists.getByTitle("BigList").items.filter(`ID gt 6000`)().then(r => {
    +  this.domElement.innerHTML += r.map(l => `${l.Title}<br />`);
    +});
    +
    +

    Selective Use

    +

    Starting with version 2.0.2 you can selectively include the polyfills from the package. Depending on your needs it may make sense in your application to use the underlying libraries directly. We have added an expanded statement on our polyfills.

    +
    // individually include polyfills as needed to match your requirements
    +import "@pnp/polyfill-ie11/dist/fetch";
    +import "@pnp/polyfill-ie11/dist/fill";
    +import "@pnp/polyfill-ie11/dist/from";
    +import "@pnp/polyfill-ie11/dist/iterator";
    +import "@pnp/polyfill-ie11/dist/map";
    +import "@pnp/polyfill-ie11/dist/promise";
    +import "@pnp/polyfill-ie11/dist/reflect";
    +import "@pnp/polyfill-ie11/dist/symbol";
    +
    +
    +// works in IE11 and other browsers
    +sp.web.lists.getByTitle("BigList").items.filter(`ID gt 6000`)().then(r => {
    +  this.domElement.innerHTML += r.map(l => `${l.Title}<br />`);
    +});
    +
    +

    SearchQueryBuilder

    +

    Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version as shown below.

    +
    import "@pnp/polyfill-ie11";
    +import { SearchQueryBuilder } from "@pnp/polyfill-ie11/dist/searchquerybuilder";
    +import { sp, ISearchQueryBuilder } from "@pnp/sp/presets/all";
    +
    +// works in IE11 and other browsers
    +const builder: ISearchQueryBuilder = SearchQueryBuilder().text("test");
    +
    +sp.search(builder).then(r => {
    +  this.domElement.innerHTML = JSON.stringify(r);
    +});
    +
    +

    General Statement on Polyfills

    +

    Internet Explorer 11 (IE11) has been an enterprise standard browser for many years. Given the complexity in changing technical platforms in many organizations, it is no surprise standardization on this out-of-date browser continues. Unfortunately, for those organizations, the Internet has moved on and many - if not all - SaaS platforms are embracing modern standards and no longer supporting the legacy IE11 browser. Even Microsoft states in their official documentation that Microsoft 365 is best experienced with a modern browser. They have even gone so far to build the latest version of Microsoft Edge based on Chromium (Edge Chromium), with an "Internet Explorer mode" allowing organizations to load legacy sites which require IE automatically.

    +

    PnPjs is now "modern" as well, and by that we mean we have moved to using capabilities of current browsers and JavaScript which are not present in IE11. We understand as a developer your ability to require an organization to switch browsers is unrealistic. We want to do everything we can to support you, but it is up to you to ensure your application is properly supported in IE11.

    +

    There are many polyfills available, depending on the platform you're running on, the frameworks you are using, and the libraries you consume. Although the majority of PnPjs users build for SharePoint Online, a significant number build for earlier versions of the platform as well as for their own node-based solutions or websites. Unfortunately, there is no way our polyfill library can support all these scenarios.

    +

    What we intended with the @pnp/polyfill-ie11 package was to provide a comprehensive group of all the polyfills that would be needed based on the complete PnPjs library. We are finding when we aggregate our polyfills with the polyfills provided in the SharePoint page and from other sources, things don't always work well. We cannot solve this for your specific situations except by providing you transparency into the polyfills which we know are necessary for our packages. You may need to adjust what polyfills your application uses based on the other libraries you are using.

    +

    To that end, we want to provide the list of polyfills we recommend here - along with the associated packages – with the goal of helping you to work out what combination of polyfills might work with your code. Also, if you haven't reviewed it yet, please check out the information on IE11 Mode for how to configure IE11 mode in the sp.setup as well as what limitations doing so will have on your usage of PnPjs.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    imports
    import "core-js/stable/array/from";
    import "core-js/stable/array/fill";
    import "core-js/stable/array/iterator";
    import "core-js/stable/promise";
    import "core-js/stable/reflect";
    import "es6-map/implement";
    import "core-js/stable/symbol";
    import "whatwg-fetch";
    +

    The following NPM packages are what we use to do the above indicated imports +|package| +|---| +|core-js| +|es6-map| +|whatwg-fetch|

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/selective-imports/index.html b/v2/concepts/selective-imports/index.html new file mode 100644 index 000000000..3de62dbe5 --- /dev/null +++ b/v2/concepts/selective-imports/index.html @@ -0,0 +1,2360 @@ + + + + + + + + + + + + + + + + + + Selective Imports - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Selective Imports

    +

    As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports in v2. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking.

    +

    This concept works well with custom bundling to create a shared package tailored exactly to your needs.

    +

    If you would prefer to not worry about selective imports please see the section on presets.

    +

    Old way

    +
    // the sp var came with all library functionality already attached
    +// meaning treeshaking couldn't reduce the size
    +import { sp } from "@pnp/sp";
    +
    +const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
    +
    +

    New Way

    +
    // the sp var now has almost nothing attached at import time and relies on
    +import { sp } from "@pnp/sp";
    +// we need to import each of the pieces we need to "attach" them for chaining
    +// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items/list";
    +
    +const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
    +
    +

    Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific

    +
    // the sp var now has almost nothing attached at import time and relies on
    +import { sp } from "@pnp/sp";
    +// we need to import each of the pieces we need to "attach" them for chaining
    +// here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();
    +
    +

    The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example:

    +
    // this import statement will attach content-type functionality to list, web, and item
    +import "@pnp/sp/content-types";
    +
    +// this import statement will only attach content-type functionality to web
    +import "@pnp/sp/content-types/web";
    +
    +

    If you only need to access content types on the web object you can reduce size by only importing that piece.

    +
    // this will fail
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { IList } from "@pnp/sp/lists";
    +
    +// do this instead
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import { IList } from "@pnp/sp/lists";
    +
    +const lists = await sp.web.lists();
    +
    +

    Presets

    +

    Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually.

    +

    SP

    +

    For the sp library there are two presets "all" and "core". The all preset mimics the behavior in v1 and includes everything in the library already attached to the sp var.

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +// sp.* exists as it did in v1, tree shaking will not work
    +const lists = await sp.web.lists();
    +
    +

    The "core" preset includes sites, webs, lists, and items.

    +
    import { sp } from "@pnp/sp/presets/core";
    +
    +// sp.* exists as it did in v1, tree shaking will not work
    +const lists = await sp.web.lists();
    +
    +

    Graph

    +

    The graph library contains a single preset, "all" mimicking the v1 structure.

    +
    import { graph } from "@pnp/graph/presets/all";
    +
    +// graph.* exists as it did in v1, tree shaking will not work
    +
    +
    +

    While we may look to add additional presets in the future you are encouraged to look at making your own custom bundles as a preferred solution.

    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/concepts/settings/index.html b/v2/concepts/settings/index.html new file mode 100644 index 000000000..bc98100c1 --- /dev/null +++ b/v2/concepts/settings/index.html @@ -0,0 +1,2551 @@ + + + + + + + + + + + + + + + + + + Settings - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Project Settings

    +

    This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally.

    +

    The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root.

    +

    Settings File Format (>= 2.0.13)

    +

    Starting with version 2.0.13 we have added support within the settings file for MSAL authentication for both SharePoint and Graph. You are NOT required to update your existing settings file unless you want to use MSAL authentication with a Graph application. The existing id/secret settings continue to work however we recommend updating when you have an opportunity. For more information coinfiguring MSAL please review the section in the authentication section for node.

    +

    MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always "https://{tenant}.sharepoint.com/.default" or "https://graph.microsoft.com/.default" depending on what you are calling.

    +
    +

    If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated.

    +
    +
    const privateKey = `-----BEGIN RSA PRIVATE KEY-----
    +your private key, read from a file or included here
    +-----END RSA PRIVATE KEY-----
    +`;
    +
    +var msalInit = {
    +    auth: {
    +        authority: "https://login.microsoftonline.com/{tenant id}",
    +        clientCertificate: {
    +            thumbprint: "{certificate thumbnail}",
    +            privateKey: privateKey,
    +        },
    +        clientId: "{AAD App registration id}",
    +    }
    +}
    +
    +var settings = {
    +    testing: {
    +        enableWebTests: true,
    +        testUser: "i:0#.f|membership|user@consto.com",
    +        sp: {
    +            url: "{required for MSAL - absolute url of test site}",
    +            notificationUrl: "{ optional: notification url }",
    +            msal: {
    +                init: msalInit,
    +                scopes: ["https://{tenant}.sharepoint.com/.default"]
    +            },
    +        },
    +        graph: {
    +            msal: {
    +                init: msalInit,
    +                scopes: ["https://graph.microsoft.com/.default"]
    +            },
    +        },
    +    },
    +}
    +
    +module.exports = settings;
    +
    +

    The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    enableWebTestsFlag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required.
    testUserAAD login account to be used when running tests.
    spSettings used to configure SharePoint (sp library) debugging and tests
    graphSettings used to configure Microsoft Graph (graph library) debugging and tests
    +

    SP values

    + + + + + + + + + + + + + + + + + + + + + +
    namedescription
    urlThe url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details.
    notificationUrlUrl used when registering test subscriptions
    msalInformation about MSAL authentication setup
    +

    Graph value

    +

    The graph values are described in the table below and come from registering an AAD Application. The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against.

    + + + + + + + + + + + + + +
    namedescription
    msalInformation about MSAL authentication setup
    +

    Settings File Format (<= 2.0.12)

    +
    var settings = {
    +
    +    testing: {
    +        enableWebTests: true,
    +        sp: {
    +            id: "{ client id }",
    +            secret: "{ client secret }",
    +            url: "{ site collection url }",
    +            notificationUrl: "{ optional: notification url }",
    +        },
    +        graph: {
    +            tenant: "{tenant.onmicrosoft.com}",
    +            id: "{your app id}",
    +            secret: "{your secret}"
    +        },
    +    }
    +}
    +
    +module.exports = settings;
    +
    + + + + + + + + + + + + + + + + + + + + + +
    enableWebTestsFlag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required.
    spSettings used to configure SharePoint (sp library) debugging and tests
    graphSettings used to configure Microsoft Graph (graph library) debugging and tests
    +

    SP values

    +

    The sp values are described in the table below and come from registering a legacy SharePoint add-in.

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    namedescription
    idThe client id of the registered application
    secretThe client secret of the registered application
    urlThe url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details.
    notificationUrlUrl used when registering test subscriptions
    +

    Graph values

    +

    The graph values are described in the table below and come from registering an AAD Application. The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against.

    + + + + + + + + + + + + + + + + + + + + + +
    namedescription
    tenantTenant to target for authentication and data (ex: contoso.onmicrosoft.com)
    idThe application id
    secretThe application secret
    +

    Create Settings.js file

    +
      +
    1. Copy the example file and rename it settings.js. Place the file in the root of your project.
    2. +
    3. Update the settings as needed for your environment.
    4. +
    +
    +

    If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.

    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/config-store/configuration/index.html b/v2/config-store/configuration/index.html new file mode 100644 index 000000000..3e29e0cf8 --- /dev/null +++ b/v2/config-store/configuration/index.html @@ -0,0 +1,2226 @@ + + + + + + + + + + + + + + + + + + configuration - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/config-store/configuration

    +

    The main class exported from the config-store package is Settings. This is the class through which you will load and access your +settings via providers.

    +
    import { Web } from "@pnp/sp/presets/all";
    +import { Settings, SPListConfigurationProvider } from "@pnp/config-store";
    +
    +// create an instance of the settings class, could be static and shared across your application
    +// or built as needed.
    +const settings = new Settings();
    +
    +// you can add/update a single value using add
    +settings.add("mykey", "myvalue");
    +
    +// you can also add/update a JSON value which will be stringified for you as a shorthand
    +settings.addJSON("mykey2", {
    +    field: 1,
    +    field2: 2,
    +    field3: 3,
    +});
    +
    +// and you can apply a plain object of keys/values that will be written as single values
    +// this results in each enumerable property of the supplied object being added to the settings collection
    +settings.apply({
    +    field: 1,
    +    field2: 2,
    +    field3: 3,
    +});
    +
    +// and finally you can load values from a configuration provider
    +const w = Web("https://mytenant.sharepoint.com/sites/dev");
    +const provider = new SPListConfigurationProvider(w, "myconfiglistname");
    +
    +// this will load values from the supplied list
    +// by default the key will be from the Title field and the value from a column named Value
    +await settings.load(provider);
    +
    +// once we have loaded values we can then read them
    +const value = settings.get("mykey");
    +
    +// or read JSON that will be parsed for you from the store
    +const value2 = settings.getJSON("mykey2");
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/config-store/index.html b/v2/config-store/index.html new file mode 100644 index 000000000..9d276e595 --- /dev/null +++ b/v2/config-store/index.html @@ -0,0 +1,2239 @@ + + + + + + + + + + + + + + + + + + config-store - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/config-store

    +

    npm version

    +

    This module provides a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed.

    +

    Getting Started

    +

    Install the library and required dependencies

    +

    npm install @pnp/sp @pnp/config-store --save

    +

    See the topics below for usage:

    + + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/config-store/providers/index.html b/v2/config-store/providers/index.html new file mode 100644 index 000000000..667143119 --- /dev/null +++ b/v2/config-store/providers/index.html @@ -0,0 +1,2275 @@ + + + + + + + + + + + + + + + + + + providers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/config-store/providers

    +

    Currently there is a single provider included in the library, but contributions of additional providers are welcome.

    +

    SPListConfigurationProvider

    +

    This provider is based on a SharePoint list it reads all of the rows and makes them available as a TypedHash<string>. By default the column names used are Title for key and "Value" for value, but you can update these as needed. Additionally, the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence.

    +
    import { Web } from "@pnp/sp/presets/all";
    +import { Settings, SPListConfigurationProvider } from "@pnp/config-store";
    +
    +// create a new provider instance
    +const w = Web("https://mytenant.sharepoint.com/sites/dev");
    +const provider = new SPListConfigurationProvider(w, "myconfiglistname");
    +
    +const settings = new Settings();
    +
    +// load our values from the list
    +await settings.load(provider);
    +
    +

    CachingConfigurationProvider

    +

    Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a +provider and caches the configuration in local or session storage.

    +
    import { Web } from "@pnp/sp/presets/all";
    +import { Settings, SPListConfigurationProvider } from "@pnp/config-store";
    +
    +// create a new provider instance
    +const w = Web("https://mytenant.sharepoint.com/sites/dev");
    +const provider = new SPListConfigurationProvider(w, "myconfiglistname");
    +
    +// get an instance of the provider wrapped
    +// you can optionally provide a key that will be used in the cache to the asCaching method
    +const wrappedProvider = provider.asCaching();
    +
    +// use that wrapped provider to populate the settings
    +await settings.load(wrappedProvider);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/debug-tests/index.html b/v2/contributing/debug-tests/index.html new file mode 100644 index 000000000..27212451c --- /dev/null +++ b/v2/contributing/debug-tests/index.html @@ -0,0 +1,2323 @@ + + + + + + + + + + + + + + + + + + Writing Tests - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Writing Tests

    +

    With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.

    +

    How to write Tests

    +

    We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts:

    +
    import { getRandomString } from "@pnp/core";
    +import { testSettings } from "../main";
    +import { expect } from "chai";
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items/list";
    +import { IList } from "@pnp/sp/lists";
    +
    +describe("Items", () => {
    +
    +    // any tests that make a web request should be withing a block checking if web tests are enabled
    +    if (testSettings.enableWebTests) {
    +
    +        // a block scoped var we will use across our tests
    +        let list: IList = null;
    +
    +        // we use the before block to setup
    +        // executed before all the tests in this block, see the mocha docs for more details
    +        // mocha prefers using function vs arrow functions and this is recommended
    +        before(async function () {
    +
    +            // execute a request to ensure we have a list
    +            const ler = await sp.web.lists.ensure("ItemTestList", "Used to test item operations");
    +            list = ler.list;
    +
    +            // in this case we want to have some items in the list for testing so we add those
    +            // only if the list was just created
    +            if (ler.created) {
    +
    +                // add a few items to get started
    +                const batch = sp.web.createBatch();
    +                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
    +                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
    +                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
    +                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
    +                list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` });
    +                await batch.execute();
    +            }
    +        });
    +
    +        // this test has a label "get items" and is run via an async function
    +        it("get items", async function () {
    +
    +            // make a request for the list's items
    +            const items = await list.items();
    +
    +            // report that we expect that result to be an array with more than 0 items
    +            expect(items.length).to.be.gt(0);
    +        });
    +
    +        // ... remainder of code removed
    +    }
    +}
    +
    +

    General Guidelines for Writing Tests

    +
      +
    • Tests should operate within the site defined in testSettings
    • +
    • Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves
    • +
    • Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll
    • +
    • When writing tests you can use "only" and "skip" from mochajs to focus on only the tests you are writing
    • +
    • Be sure to review the various options when running your tests
    • +
    • If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description
    • +
    +

    Next Steps

    +

    Now that you've written tests to cover your changes you'll need to update the docs.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/debugging/index.html b/v2/contributing/debugging/index.html new file mode 100644 index 000000000..fce32ec56 --- /dev/null +++ b/v2/contributing/debugging/index.html @@ -0,0 +1,2514 @@ + + + + + + + + + + + + + + + + + + Debugging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    Debugging

    +

    Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on.

    +

    Before proceeding be sure you have reviewed how to setup for local configuration and debugging.

    +

    Debugging Library Features

    +

    The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.

    +

    Basic SharePoint Testing

    +

    You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules.

    +

    All of the setup for the node client is handled within sp.ts using the settings from the local configuration.

    +

    Basic Graph Testing

    +

    Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit.

    +

    All of the setup for the node client is handled within graph.ts using the settings from the local configuration.

    +

    How to: Create a Debug Module

    +

    If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git.

    +

    Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content:

    +
    // note we can use the actual package names for our imports (ex: @pnp/logging)
    +import { Logger, LogLevel, ConsoleListener } from "@pnp/logging";
    +// using the all preset for simplicity in the example, selective imports work as expected
    +import { sp, ListEnsureResult } from "@pnp/sp/presets/all";
    +
    +declare var process: { exit(code?: number): void };
    +
    +export async function MyDebug() {
    +
    +  // configure your options
    +  // you can have different configs in different modules as needed for your testing/dev work
    +  sp.setup({
    +    sp: {
    +      fetchClientFactory: () => {
    +        return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret);
    +      },
    +    },
    +  });
    +
    +  // run some debugging
    +  const list = await sp.web.lists.ensure("MyFirstList");
    +
    +  Logger.log({
    +    data: list.created,
    +    level: LogLevel.Info,
    +    message: "Was list created?",
    +  });
    +
    +  if (list.created) {
    +
    +    Logger.log({
    +      data: list.data,
    +      level: LogLevel.Info,
    +      message: "Raw data from list creation.",
    +    });
    +
    +  } else {
    +
    +    Logger.log({
    +      data: null,
    +      level: LogLevel.Info,
    +      message: "List already existed!",
    +    });
    +  }
    +
    +  process.exit(0);
    +}
    +
    +

    Update main.ts to launch your module

    +

    First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this:

    +
    // ...
    +
    +// comment out the example
    +// import { Example } from "./example";
    +// Example();
    +
    +import { MyDebug } from "./mydebug"
    +MyDebug();
    +
    +// ...
    +
    +
    +

    Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)

    +
    +

    Debug

    +

    Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.

    +

    Debug Module Next Steps

    +

    Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.

    +

    In Browser Debugging

    +

    You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js, allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.

    +

    Start the local serve

    +

    This will serve a package with ./debug/serve/main.ts as the entry.

    +

    gulp serve

    +

    Add reference to library

    +

    Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.

    +
    <script src="https://localhost:8080/assets/pnp.js"></script>
    +<div id="pnp-test"></div>
    +
    +

    You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.

    +

    Debug

    +

    Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.

    +

    Next Steps

    +

    You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser.

    +

    Now you can learn about extending the library.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/documentation/index.html b/v2/contributing/documentation/index.html new file mode 100644 index 000000000..fd2a5cf8c --- /dev/null +++ b/v2/contributing/documentation/index.html @@ -0,0 +1,2291 @@ + + + + + + + + + + + + + + + + + + Update Documentation - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Documentation

    +

    Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.

    +

    Writing Docs

    +

    Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.

    +

    Building Docs Locally

    +

    Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable.

    +

    When executing the pip module on Windows you can prefix it with python -m. +For example:

    +

    python -m pip install mkdocs-material

    +
      +
    • Install MkDocs
        +
      • pip install mkdocs
      • +
      +
    • +
    • Install the Material theme
        +
      • pip install mkdocs-material
      • +
      +
    • +
    • install the mkdocs-markdownextradata-plugin - this is used for the version variable
        +
      • pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7)
      • +
      +
    • +
    • install redirect plugin - used to redirect from moved pages
        +
      • pip install mkdocs-redirects
      • +
      +
    • +
    • Serve it up
        +
      • mkdocs serve
      • +
      • Open a browser to http://127.0.0.1:8000/
      • +
      +
    • +
    +
    +

    Please see the official mkdocs site for more details on working with mkdocs

    +
    +

    Next Steps

    +

    After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request!

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/extending-the-library/index.html b/v2/contributing/extending-the-library/index.html new file mode 100644 index 000000000..c638f44bd --- /dev/null +++ b/v2/contributing/extending-the-library/index.html @@ -0,0 +1,2521 @@ + + + + + + + + + + + + + + + + + + Extending the library - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    + +
    + + +
    +
    + + + + + + + +

    Extending PnPjs

    +
    +

    This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property.

    +
    +

    At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the "webs" property is added to the web class.

    +
    // TypeScript property, returning an interface
    +public get webs(): IWebs {
    +    // using the Webs factory function and providing "this" as the first parameter
    +    return Webs(this);
    +}
    +
    +

    Understanding Factory Functions

    +

    PnPjs v2 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form.

    +
    // create a constant which is a function of type ISPInvokableFactory having the name Webs
    +// this is bound by the generic type param to return an IWebs instance
    +// and it will use the _Webs concrete class to form the internal type of the invocable
    +export const Webs = spInvokableFactory<IWebs>(_Webs);
    +
    +

    The ISPInvokableFactory type looks like:

    +
    export type ISPInvokableFactory<R = any> = (baseUrl: string | ISharePointQueryable, path?: string) => R;
    +
    +

    And the matching graph type:

    +
    <R>(f: any): (baseUrl: string | IGraphQueryable, path?: string) => R
    +
    +

    The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples.

    +
    import { Web } from "@pnp/sp/webs";
    +
    +// create a web from an absolute url
    +const web = Web("https://tenant.sharepoint.com");
    +
    +// as an example, create a new web using the first as a base
    +// targets: https://tenant.sharepoint.com/sites/dev
    +const web2 = Web(web, "sites/dev");
    +
    +// or you can add any path components you want, here as an example we access the current user property
    +const cu = Web(web, "currentuser");
    +const currentUserInfo = cu();
    +
    +

    Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their "type". It is an important concept when working with the library to always remember we are just building strings.

    +

    Class structure

    +

    Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method

    +
    /*
    +The concrete class implementation. This is never exported or shown directly
    +to consumers of the library. It is wrapped by the Proxy we do expose.
    +
    +It extends the _SharePointQueryableInstance class for which there is a matching
    +_SharePointQueryableCollection. The generic parameter defines the return type
    +of a get operation and the invoked result.
    +
    +Classes can have methods and properties as normal. This one has a single property as a simple example
    +*/
    +export class _HubSite extends _SharePointQueryableInstance<IHubSiteInfo> {
    +
    +    /**
    +     * Gets the ISite instance associated with this hub site
    +     */
    +    // the tag decorator is used to provide some additional telemetry on what methods are
    +    // being called.
    +    @tag("hs.getSite")
    +    public async getSite(): Promise<ISite> {
    +
    +        // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result
    +        const d = await this.select("SiteUrl")();
    +
    +        // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl
    +        return Site(d.SiteUrl);
    +    }
    +}
    +
    +/*
    +This defines the interface we export and expose to consumers.
    +In most cases this extends the concrete object but may add or remove some methods/properties
    +in special cases
    +*/
    +export interface IHubSite extends _HubSite { }
    +
    +/*
    +This defines the HubSite factory function as discussed above
    +binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite.
    +
    +This is understood to mean that HubSite is a factory function that returns a types of IHubSite
    +which the spInvokableFactory will create using _HubSite as the concrete underlying type.
    +*/
    +export const HubSite = spInvokableFactory<IHubSite>(_HubSite);
    +
    +

    Add a Property

    +

    In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class.

    +
    export class _View extends _SharePointQueryableInstance<IViewInfo> {
    +
    +    // ... other code removed
    +
    +    // add the property, and provide a return type
    +    // return types should be interfaces
    +    public get fields(): IViewFields {
    +        // we use the ViewFields factory function supplying "this" as the first parameter
    +        // this will create a url like ".../fields/viewfields" due to the defaultPath decorator
    +        // on the _ViewFields class. This is equivalent to: ViewFields(this, "viewfields")
    +        return ViewFields(this);
    +    }
    +
    +    // ... other code removed
    +}
    +
    +
    +

    There are many examples throughout the library that follow this pattern.

    +
    +

    Add a Method

    +

    Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method:

    +
    @defaultPath("items")
    +export class _Items extends _SharePointQueryableCollection {
    +
    +    /**
    +    * Gets an Item by id
    +    *
    +    * @param id The integer id of the item to retrieve
    +    */
    +    // we declare a method and set the return type to an interface
    +    public getById(id: number): IItem {
    +        // here we use the tag helper to add some telemetry to our request
    +        // we create a new IItem using the factory and appending the id value to the end
    +        // this gives us a valid url path to a single item .../items/getById(2)
    +        // we can then use the returned IItem to extend our chain or execute a request
    +        return tag.configure(Item(this).concat(`(${id})`), "is.getById");
    +    }
    +
    +    // ... other code removed
    +}
    +
    +

    Web Request Method

    +

    A second example is a method that performs a request. Here we use the _Item recycle method as an example:

    +
    /**
    + * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item.
    + */
    +// we use the tag decorator to add telemetry
    +@tag("i.recycle")
    +// we return a promise
    +public recycle(): Promise<string> {
    +    // we use the spPost method to post the request created by cloning our current instance IItem using
    +    // the Item factory and adding the path "recycle" to the end. Url will look like .../items/getById(2)/recycle
    +    return spPost<string>(this.clone(Item, "recycle"));
    +}
    +
    +

    Augment Using Selective Imports

    +

    To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available.

    +
    // import the addProp helper
    +import { addProp } from "@pnp/queryable";
    +// import the _List concrete class from the types module (not the index!)
    +import { _List } from "../lists/types";
    +// import the interface and factory we are going to add to the List
    +import { Items, IItems } from "./types";
    +
    +// This module declaration fixes up the types, allowing .items to appear in intellisense
    +// when you import "@pnp/sp/items/list";
    +declare module "../lists/types" {
    +    // we need to extend the concrete type
    +    interface _List {
    +        readonly items: IItems;
    +    }
    +    // we need to extend the interface
    +    // this may not be strictly necessary as the IList interface extends _List so it
    +    // should pick up the same additions, but we have seen in some cases this does seem
    +    // to be required. So we include it for safety as it will all be removed during
    +    // transpilation we don't need to care about the extra code
    +    interface IList {
    +        readonly items: IItems;
    +    }
    +}
    +
    +// finally we add the property to the _List class
    +// this method call says add a property to _List named "items" and that property returns a result using the Items factory
    +// The factory will be called with "this" when the property is accessed. If needed there is a fourth parameter to append additional path
    +// information to the property url
    +addProp(_List, "items", Items);
    +
    +

    General Rules for Extending PnPjs

    +
      +
    • Only expose interfaces to consumers
    • +
    • Use the factory functions except in very special cases
    • +
    • Look for other properties and methods as examples
    • +
    • Simple is always preferable, but not always possible - use your best judgement
    • +
    • If you find yourself writing a ton of code to solve a problem you think should be easy, ask
    • +
    • If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed
    • +
    +

    Next Steps

    +

    Now that you have extended the library you need to write a test to cover it!

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/index.html b/v2/contributing/index.html new file mode 100644 index 000000000..c72642646 --- /dev/null +++ b/v2/contributing/index.html @@ -0,0 +1,2270 @@ + + + + + + + + + + + + + + + + + + Contributing - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Contributing to PnPjs

    +

    Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    SectionDescription
    Setup Dev MachineCovers setting up your machine to ensure you are ready to debug the solution
    Local Debug ConfigurationDiscusses the steps required to establish local configuration used for debugging and running tests
    DebuggingDescribes how to debug PnPjs locally
    Extending the libraryBasic examples on how to extend the library such as adding a method or property
    Writing TestsHow to write and debug tests
    Update DocumentationDescribes the steps required to edit and locally view the documentation
    Submit a Pull RequestOutlines guidance for submitting a pull request
    +

    Need Help?

    +

    The PnP "Sharing Is Caring" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.

    +

    Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.

    +

    To learn more and register for an upcoming session, please visit the Sharing is Caring website.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/local-debug-configuration/index.html b/v2/contributing/local-debug-configuration/index.html new file mode 100644 index 000000000..900e13f6e --- /dev/null +++ b/v2/contributing/local-debug-configuration/index.html @@ -0,0 +1,2284 @@ + + + + + + + + + + + + + + + + + + Local Debug Configuration - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Local Debugging Configuration

    +

    This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).

    +

    Create settings.js

    +

    Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. +For more information the settings file please see Settings

    +

    Minimal Configuration

    +

    You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag.

    +

    The following configuration file allows you to run all the tests that do not contact services.

    +
     var sets = {
    +     testing: {
    +         enableWebTests: false,
    +     }
    + }
    +
    +module.exports = sets;
    +
    +

    Test your setup

    +

    If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/pull-requests/index.html b/v2/contributing/pull-requests/index.html new file mode 100644 index 000000000..9c67e59e9 --- /dev/null +++ b/v2/contributing/pull-requests/index.html @@ -0,0 +1,2261 @@ + + + + + + + + + + + + + + + + + + Submit a Pull Request - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Submitting Pull Requests

    +

    Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release.

    +
      +
    • Target your pull requests to the version-2 branch
    • +
    • Add/Update any relevant docs articles in the relevant package's docs folder related to your changes
    • +
    • Include a test for any new functionality and ensure all existing tests are passing by running npm test
    • +
    • Ensure linting checks pass by typing npm run lint
    • +
    • Ensure everything works for a build by running npm run package
    • +
    • Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work
    • +
    • If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :)
    • +
    +
    +

    If you need to target a PR for version 1, please target the "version-1" branch

    +
    +

    Sharing is Caring - Pull Request Guidance

    +

    The PnP "Sharing Is Caring" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs.

    +

    Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives.

    +

    To learn more and register for an upcoming session, please visit the Sharing is Caring website.

    +

    Next Steps

    +

    Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted.

    +

    Thank you for helping PnPjs grow and improve!!

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/contributing/setup-dev-machine/index.html b/v2/contributing/setup-dev-machine/index.html new file mode 100644 index 000000000..5b6131020 --- /dev/null +++ b/v2/contributing/setup-dev-machine/index.html @@ -0,0 +1,2284 @@ + + + + + + + + + + + + + + + + + + Setup Dev Machine - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Setting up your Developer Machine

    +

    If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging.

    +

    Setup your development environment

    +

    These steps will help you get your environment setup for contributing to the core library.

    +
      +
    1. +

      Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like.

      +
    2. +
    3. +

      Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget).

      +
      +

      This library requires node >= 10.18.0

      +
      +
    4. +
    5. +

      On Windows: Install Python

      +
    6. +
    7. +

      [Optional] Install the tslint extension in VS Code:

      +
        +
      1. Press Shift + Ctrl + "p" to open the command panel
      2. +
      3. Begin typing "install extension" and select the command when it appears in view
      4. +
      5. Begin typing "tslint" and select the package when it appears in view
      6. +
      7. Restart Code after installation
      8. +
      +
    8. +
    +

    Fork The Repo

    +

    All of our contributions come via pull requests and you'll need to fork the repository

    +
      +
    1. +

      Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool.

      +
    2. +
    3. +

      Once you have the code locally, navigate to the root of the project in your console. Type the following command:

      +

      npm install

      +
    4. +
    5. +

      Follow the guidance to complete the one-time local configuration required to debug and run tests.

      +
    6. +
    7. +

      Then you can follow the guidance in the debugging article.

      +
    8. +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/css/extra.css b/v2/css/extra.css new file mode 100644 index 000000000..6391b1ff7 --- /dev/null +++ b/v2/css/extra.css @@ -0,0 +1,33 @@ +.md-logo { + height: 32px; + width: 150px; + padding: 0 0.25 0.5 !important; +} + +.md-header{ + height: 75px; +} + +.md-container{ + padding-top: 70px; +} + +.md-sidebar[data-md-state="lock"]{ + padding-top: 75px; +} + +.md-logo img { + width: 100% !important; + height: auto !important; + margin-top: -0.25em; +} + +.md-footer { + margin-top: 5em; +} + +@media only screen and (max-width: 76.1875em) { + .md-nav--primary .md-nav__title--site .md-nav__button { + width: 150px; + } +} \ No newline at end of file diff --git a/v2/debug-tests/index.html b/v2/debug-tests/index.html new file mode 100644 index 000000000..00fa06798 --- /dev/null +++ b/v2/debug-tests/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/v2/debugging/index.html b/v2/debugging/index.html new file mode 100644 index 000000000..cc419d1b0 --- /dev/null +++ b/v2/debugging/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/v2/documentation/index.html b/v2/documentation/index.html new file mode 100644 index 000000000..58a139e9c --- /dev/null +++ b/v2/documentation/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/v2/getting-started-dev/index.html b/v2/getting-started-dev/index.html new file mode 100644 index 000000000..a93ff396c --- /dev/null +++ b/v2/getting-started-dev/index.html @@ -0,0 +1,15 @@ + + + + + + Redirecting... + + + + + + +Redirecting... + + diff --git a/v2/getting-started/index.html b/v2/getting-started/index.html new file mode 100644 index 000000000..8eda6173a --- /dev/null +++ b/v2/getting-started/index.html @@ -0,0 +1,2789 @@ + + + + + + + + + + + + + + + + + + Getting Started - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    Getting Started

    +

    These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number.

    +

    If you need to support older browsers please review the article on polyfills for required functionality.

    +

    Install

    +

    First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project.

    +

    npm install @pnp/sp @pnp/graph --save

    +

    Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation +for more details and examples.

    +
    import { getRandomString } from "@pnp/core";
    +
    +(function() {
    +
    +  // get and log a random string
    +  console.log(getRandomString(20));
    +
    +})()
    +
    +

    Getting Started with SharePoint Framework

    +

    The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises please read this note on a workaround for the included TypeScript version. If you are targeting SharePoint online you do not need to take any additional steps.

    +

    Establish Context

    +

    Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the SPFx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports.

    +

    The setup is always done in the onInit method to ensure it runs before your other life-cycle code. You can also set any other settings at this time.

    +

    Using @pnp/core setup

    +
    import { setup as pnpSetup } from "@pnp/core";
    +
    +// ...
    +
    +protected onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +
    +    pnpSetup({
    +      spfxContext: this.context
    +    });
    +  });
    +}
    +
    +// ...
    +
    +
    +

    Using @pnp/sp setup

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +// ...
    +
    +protected onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +
    +    sp.setup({
    +      spfxContext: this.context
    +    });
    +  });
    +}
    +
    +// ...
    +
    +
    +

    Sp setup also supports passing just the SPFx context object directly as this is the most common case

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +// ...
    +
    +protected async onInit(): Promise<void> {
    +
    +  await super.onInit();
    +
    +  // other init code may be present
    +
    +  sp.setup(this.context);
    +}
    +
    +// ...
    +
    +
    +

    Using @pnp/graph setup

    +
    import { graph } from "@pnp/graph/presets/all";
    +
    +// ...
    +
    +protected onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +
    +    graph.setup({
    +      spfxContext: this.context
    +    });
    +  });
    +}
    +
    +// ...
    +
    +
    +

    Establish context within an SPFx service

    +

    Because you do not have full access to the context object within a service you need to setup things a little differently. If you do not need AAD tokens you can leave that part out and specify just the pageContext.

    +
    import { ServiceKey, ServiceScope } from "@microsoft/sp-core-library";
    +import { PageContext } from "@microsoft/sp-page-context";
    +import { AadTokenProviderFactory } from "@microsoft/sp-http";
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +
    +export interface ISampleService {
    +  getLists(): Promise<any[]>;
    +}
    +
    +export class SampleService {
    +
    +  public static readonly serviceKey: ServiceKey<ISampleService> = ServiceKey.create<ISampleService>('SPFx:SampleService', SampleService);
    +
    +  constructor(serviceScope: ServiceScope) {
    +
    +    serviceScope.whenFinished(() => {
    +
    +      const pageContext = serviceScope.consume(PageContext.serviceKey);
    +      const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey);
    +
    +      // we need to "spoof" the context object with the parts we need for PnPjs
    +      sp.setup({
    +        spfxContext: {
    +          aadTokenProviderFactory: tokenProviderFactory,
    +          pageContext: pageContext,
    +        }
    +      });
    +
    +      // This approach also works if you do not require AAD tokens
    +      // you don't need to do both
    +      // sp.setup({
    +      //   sp : {
    +      //     baseUrl : pageContext.web.absoluteUrl
    +      //   }
    +      // });
    +    });
    +  }
    +  public getLists(): Promise<any[]> {
    +    return sp.web.lists();
    +  }
    +}
    +
    +

    Connect to SharePoint from Node

    +
    +

    Please see the main article on how we support node versions that require commonjs modules.

    +
    +

    npm i @pnp/sp-commonjs @pnp/nodejs-commonjs

    +

    This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. +Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports:

    +
    import { sp } from "@pnp/sp-commonjs";
    +import { SPFetchClient } from "@pnp/nodejs-commonjs";
    +
    +

    Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint.

    +
    // configure your node options (only once in your application)
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{site url}", "{client id}", "{client secret}");
    +        },
    +    },
    +});
    +
    +// make a call to SharePoint and log it in the console
    +sp.web.select("Title", "Description")().then(w => {
    +    console.log(JSON.stringify(w, null, 4));
    +});
    +
    +

    Connect to Microsoft Graph From Node

    +

    Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see +./debug/launch/graph.ts for a live example.

    +
    npm i @pnp/graph-commonjs @pnp/nodejs-commonjs
    +
    +

    Now we need to import what we'll need to call graph

    +
    import { graph } from "@pnp/graph-commonjs";
    +import { AdalFetchClient } from "@pnp/nodejs-commonjs";
    +
    +

    Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions.

    +
    graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +            return new AdalFetchClient("{mytenant}.onmicrosoft.com", "{application id}", "{application secret}");
    +        },
    +    },
    +});
    +
    +// make a call to Graph and get all the groups
    +graph.groups().then(g => {
    +    console.log(JSON.stringify(g, null, 4));
    +});
    +
    +

    Getting Started outside SharePoint Framework

    +

    In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options.

    +

    Set baseUrl through setup

    +

    Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when +working against unpatched versions of SharePoint 2013 as discussed here. +This is optional for 2016 or SharePoint Online. The library does not support setting the headers to use nometadata as we rely on the metadata in the response to do some of the more complicated functions. Some of the pure data calls will probably work but it is not a supported configuration.

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +sp.setup({
    +  sp: {
    +    headers: {
    +      Accept: "application/json;odata=verbose",
    +    },
    +    baseUrl: "{Absolute SharePoint Web URL}"
    +  },
    +});
    +
    +const w = await sp.web();
    +
    +

    Create Web instances directly

    +

    Using this method you create the web directly with the url you want to use as the base.

    +
    import { Web } from "@pnp/sp/presets/all";
    +
    +const web = Web("{Absolute SharePoint Web URL}");
    +const w = await web();
    +
    +

    Next Steps

    +

    Be sure to review the article describing all of the available settings across the libraries.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/calendars/index.html b/v2/graph/calendars/index.html new file mode 100644 index 000000000..51b014348 --- /dev/null +++ b/v2/graph/calendars/index.html @@ -0,0 +1,2569 @@ + + + + + + + + + + + + + + + + + + calendars - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/calendars

    +

    Calendars exist in Outlook and can belong to either a user or group. With @pnp/graph@<=2.0.6, only events for a user and group's default calendar could be fetched/created/updated. In versions 2.0.7 and up, all calendars and their events can be fetched.

    +

    More information can be found in the official Graph documentation:

    + +

    ICalendar, ICalendars

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import "@pnp/graph/calendars";
    Preset: Allimport { graph } from "@pnp/graph/presets/all";
    +

    Get All Calendars For a User

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars();
    +
    +const myCalendars = await graph.me.calendars();
    +
    +
    +

    Get a Specific Calendar For a User

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';
    +
    +const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)();
    +
    +const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();
    +
    +

    Get a User's Default Calendar

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar();
    +
    +const myCalendar = await graph.me.calendar();
    +
    +

    Get Events For a User's Default Calendar

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +// You can get the default calendar events
    +const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events();
    +// or get all events for the user
    +const events = await graph.users.getById('user@tenant.onmicrosoft.com').events();
    +
    +// You can get my default calendar events
    +const events = await graph.me.calendar.events();
    +// or get all events for me
    +const events = await graph.me.events();
    +
    +

    Get Events By ID

    +

    You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar.

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA==';
    +
    +const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA==';
    +
    +// Get events by ID
    +const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID);
    +
    +const events = await graph.me.events.getByID(EventID);
    +
    +// Get an event by ID from a specific calendar
    +const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID);
    +
    +const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);
    +
    +
    +

    Create Events

    +

    This will work on any IEvents objects (e.g. anything accessed using an events key).

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add(
    +{
    +  "subject": "Let's go for lunch",
    +  "body": {
    +    "contentType": "HTML",
    +    "content": "Does late morning work for you?"
    +  },
    +  "start": {
    +      "dateTime": "2017-04-15T12:00:00",
    +      "timeZone": "Pacific Standard Time"
    +  },
    +  "end": {
    +      "dateTime": "2017-04-15T14:00:00",
    +      "timeZone": "Pacific Standard Time"
    +  },
    +  "location":{
    +      "displayName":"Harry's Bar"
    +  },
    +  "attendees": [
    +    {
    +      "emailAddress": {
    +        "address":"samanthab@contoso.onmicrosoft.com",
    +        "name": "Samantha Booth"
    +      },
    +      "type": "required"
    +    }
    +  ]
    +});
    +
    +

    Update Events

    +

    This will work on any IEvents objects (e.g. anything accessed using an events key).

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';
    +
    +await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({
    +    reminderMinutesBeforeStart: 99,
    +});
    +
    +

    Delete Event

    +

    This will work on any IEvents objects (e.g. anything accessed using an events key).

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA=';
    +
    +await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete();
    +
    +await graph.me.events.getById(EVENT_ID).delete();
    +
    +

    Get Calendar for a Group

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/groups';
    +
    +const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();
    +
    +

    Get Events for a Group

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/groups';
    +
    +// You can do one of
    +const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events();
    +// or
    +const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();
    +
    +

    Get Calendar View

    +

    Added in 2.0.7 +Gets the events in a calendar during a specified date range.

    +
    import { graph } from '@pnp/graph';
    +import '@pnp/graph/calendars';
    +import '@pnp/graph/users';
    +
    +// basic request, note need to invoke the returned queryable
    +const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01", "2020-03-01")();
    +
    +// you can use select, top, etc to filter your returned results
    +const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01", "2020-03-01").select("subject").top(3)();
    +
    +// you can specify times along with the dates
    +const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView("2020-01-01T19:00:00-08:00", "2020-03-01T19:00:00-08:00")();
    +
    +const view4 = await graph.me.calendarView("2020-01-01", "2020-03-01")();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/contacts/index.html b/v2/graph/contacts/index.html new file mode 100644 index 000000000..2edd521d8 --- /dev/null +++ b/v2/graph/contacts/index.html @@ -0,0 +1,2673 @@ + + + + + + + + + + + + + + + + + + contacts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/contacts

    +

    The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described +you can add and edit both contacts and folders in a users Outlook.

    +

    More information can be found in the official Graph documentation:

    + +

    IContact, IContacts, IContactFolder, IContactFolders

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import "@pnp/graph/contacts";
    Preset: Allimport { graph } from "@pnp/graph/presets/all";
    +

    Set up notes

    +

    To make user calls you can use getById where the id is the users email address. +Contact ID, Folder ID, and Parent Folder ID use the following format "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA="

    +

    Get all of the Contacts

    +

    Gets a list of all the contacts for the user.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts();
    +
    +const contacts2 = await graph.me.contacts();
    +
    +
    +

    Get Contact by Id

    +

    Gets a specific contact by ID for the user.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
    +
    +const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)();
    +
    +const contact2 = await graph.me.contacts.getById(contactID)();
    +
    +
    +

    Add a new Contact

    +

    Adds a new contact for the user.

    +
    import { graph } from "@pnp/graph";
    +import { EmailAddress } from "@microsoft/microsoft-graph-types";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
    +
    +const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
    +
    +
    +

    Update a Contact

    +

    Updates a specific contact by ID for teh designated user

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
    +
    +const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: "1986-05-30" });
    +
    +const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: "1986-05-30" });
    +
    +
    +

    Delete a Contact

    +

    Delete a contact from the list of contacts for a user.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const contactID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=";
    +
    +const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete();
    +
    +const delContact2 = await graph.me.contacts.getById(contactID).delete();
    +
    +
    +

    Get all of the Contact Folders

    +

    Get all the folders for the designated user's contacts

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders();
    +
    +const contactFolders2 = await graph.me.contactFolders();
    +
    +
    +

    Get Contact Folder by Id

    +

    Get a contact folder by ID for the specified user

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +
    +const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)();
    +
    +const contactFolder2 = await graph.me.contactFolders.getById(folderID)();
    +
    +
    +

    Add a new Contact Folder

    +

    Add a new folder in the users contacts

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const parentFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=";
    +
    +const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add("New Folder", parentFolderID);
    +
    +const addedContactFolder2 = await graph.me.contactFolders.add("New Folder", parentFolderID);
    +
    +
    +

    Update a Contact Folder

    +

    Update an existing folder in the users contacts

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +
    +const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: "Updated Folder" });
    +
    +const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: "Updated Folder" });
    +
    +
    +

    Delete a Contact Folder

    +

    Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +
    +const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete();
    +
    +const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();
    +
    +
    +

    Get all of the Contacts from the Contact Folder

    +

    Get all the contacts in a folder

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +
    +const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts();
    +
    +const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();
    +
    +
    +

    Get Child Folders of the Contact Folder

    +

    Get child folders from contact folder

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +
    +const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders();
    +
    +const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();
    +
    +
    +

    Add a new Child Folder

    +

    Add a new child folder to a contact folder

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +
    +const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add("Sub Folder", folderID);
    +
    +const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add("Sub Folder", folderID);
    +
    +

    Get Child Folder by Id

    +

    Get child folder by ID from user contacts

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +const subFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=";
    +
    +const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)();
    +
    +const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();
    +
    +

    Add Contact in Child Folder of Contact Folder

    +

    Add a new contact to a child folder

    +
    import { graph } from "@pnp/graph";
    +import { EmailAddress } from "./@microsoft/microsoft-graph-types";
    +import "@pnp/graph/users"
    +import "@pnp/graph/contacts"
    +
    +const folderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=";
    +const subFolderID = "AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=";
    +
    +const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
    +
    +const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [<EmailAddress>{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/directoryobjects/index.html b/v2/graph/directoryobjects/index.html new file mode 100644 index 000000000..a9d1518ad --- /dev/null +++ b/v2/graph/directoryobjects/index.html @@ -0,0 +1,2402 @@ + + + + + + + + + + + + + + + + + + directory objects - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/directoryObjects

    +

    Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types.

    +

    More information can be found in the official Graph documentation:

    + +

    IDirectoryObject, IDirectoryObjects

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import "@pnp/graph/directory-objects";
    Preset: Allimport { graph } from "@pnp/sp/presets/all";
    +

    The groups and directory roles for the user

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +
    +const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf();
    +
    +const memberOf2 = await graph.me.memberOf();
    +
    +
    +

    Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/groups"
    +
    +const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups();
    +
    +const memberGroups2 = await graph.me.getMemberGroups();
    +
    +// Returns only security enabled groups
    +const memberGroups3 = await graph.me.getMemberGroups(true);
    +
    +const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();
    +
    +
    +

    Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/groups";
    +
    +const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects();
    +
    +const memberObjects2 = await graph.me.getMemberObjects();
    +
    +// Returns only security enabled groups
    +const memberObjects3 = await graph.me.getMemberObjects(true);
    +
    +const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();
    +
    +

    Check for membership in a specified list of groups

    +

    And returns from that list those groups of which the specified user, group, or directory object is a member

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/groups";
    +
    +const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
    +
    +const checkedMembers2 = await graph.me.checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
    +
    +const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups(["c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741","2001bb09-1d46-40a6-8176-7bb867fb75aa"]);
    +
    +

    Get directoryObject by Id

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/directory-objects";
    +
    +const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');
    +
    +
    +

    Delete directoryObject

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/directory-objects";
    +
    +const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/groups/index.html b/v2/graph/groups/index.html new file mode 100644 index 000000000..ef7438b34 --- /dev/null +++ b/v2/graph/groups/index.html @@ -0,0 +1,2488 @@ + + + + + + + + + + + + + + + + + + groups - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/groups

    +

    Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent.

    +

    Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups.

    +

    You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation.

    +

    IGroup, IGroups

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import {Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups} from "@pnp/graph/groups";
    Selective 2import { graph } from "@pnp/graph";
    import "@pnp/graph/groups";
    Preset: Allimport { graph, Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups } from "@pnp/graph/presets/all";
    +

    Add a Group

    +

    Add a new group.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +import { GroupType } from '@pnp/graph/groups';
    +
    +const groupAddResult = await graph.groups.add("GroupName", "Mail_NickName", GroupType.Office365);
    +const group = await groupAddResult.group();
    +
    +

    Delete a Group

    +

    Deletes an existing group.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").delete();
    +
    +

    Update Group Properties

    +

    Updates an existing group.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").update({ displayName: newName, propertyName: updatedValue});
    +
    +

    Add favorite

    +

    Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").addFavorite();
    +
    +

    Remove favorite

    +

    Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").removeFavorite();
    +
    +

    Reset Unseen Count

    +

    Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").resetUnseenCount();
    +
    +

    Subscribe By Mail

    +

    Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").subscribeByMail();
    +
    +

    Unsubscribe By Mail

    +

    Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").unsubscribeByMail();
    +
    +

    Get Calendar View

    +

    Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +const startDate = new Date("2020-04-01");
    +const endDate = new Date("2020-03-01");
    +
    +const events = graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").getCalendarView(startDate, endDate);
    +
    +

    Group Photo Operations

    +

    See Photos

    +

    Get the Team Site for a Group

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +import "@pnp/graph/sites/group";
    +
    +const teamSite = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").sites.root();
    +const url = teamSite.webUrl
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/index.html b/v2/graph/index.html new file mode 100644 index 000000000..96c026744 --- /dev/null +++ b/v2/graph/index.html @@ -0,0 +1,2333 @@ + + + + + + + + + + + + + + + + + + graph - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/graph

    +

    npm version

    +

    This package contains the fluent api used to call the graph rest services.

    +

    Getting Started

    +

    Install the library and required dependencies

    +

    npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save

    +

    Import the library into your application and access the root sp object

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +(function main() {
    +
    +    // here we will load the current web's properties
    +    graph.groups().then(g => {
    +
    +        console.log(`Groups: ${JSON.stringify(g, null, 4)}`);
    +    });
    +})()
    +
    +

    Getting Started with SharePoint Framework

    +

    Install the library and required dependencies

    +

    npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save

    +

    Import the library into your application, update OnInit, and access the root sp object in render

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +
    +// ...
    +
    +public onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +
    +    graph.setup({
    +      spfxContext: this.context
    +    });
    +  });
    +}
    +
    +// ...
    +
    +public render(): void {
    +
    +    // A simple loading message
    +    this.domElement.innerHTML = `Loading...`;
    +
    +    // here we will load the current web's properties
    +    graph.groups().then(groups => {
    +
    +        this.domElement.innerHTML = `Groups: <ul>${groups.map(g => `<li>${g.displayName}</li>`).join("")}</ul>`;
    +    });
    +}
    +
    +

    Getting Started on Nodejs

    +

    Install the library and required dependencies

    +

    npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save

    +

    Import the library into your application, setup the node client, make a request

    +
    import { graph } from "@pnp/graph";
    +import { AdalFetchClient } from "@pnp/nodejs";
    +import "@pnp/graph/groups";
    +
    +// do this once per page load
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +            return new AdalFetchClient("{tenant}.onmicrosoft.com", "AAD Application Id", "AAD Application Secret");
    +        },
    +    },
    +});
    +
    +// here we will load the groups information
    +graph.groups().then(g => {
    +
    +    console.log(`Groups: ${JSON.stringify(g, null, 4)}`);
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/insights/index.html b/v2/graph/insights/index.html new file mode 100644 index 000000000..c3ea73bd7 --- /dev/null +++ b/v2/graph/insights/index.html @@ -0,0 +1,2478 @@ + + + + + + + + + + + + + + + + + + insights - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/insights

    +

    This module helps you get Insights in form of Trending, Used and Shared. The results are based on relationships calculated using advanced analytics and machine learning techniques.

    +

    IInsights

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { graph } from "@pnp/graph";
    import "@pnp/graph/insights";
    Preset: Allimport "@pnp/graph/presets/all";
    + +

    Returns documents from OneDrive and SharePoint sites trending around a user.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const trending = await graph.me.insights.trending()
    +
    +const trending = await graph.users.getById("userId").insights.trending()
    +
    + +

    Using the getById method to get a trending document by Id.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const trendingDoc = await graph.me.insights.trending.getById('Id')()
    +
    +const trendingDoc = await graph.users.getById("userId").insights.trending.getById('Id')()
    +
    + +

    Using the resources method to get the resource from a trending document.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const resource = await graph.me.insights.trending.getById('Id').resource()
    +
    +const resource = await graph.users.getById("userId").insights.trending.getById('Id').resource()
    +
    +

    Get all Used documents

    +

    Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const used = await graph.me.insights.used()
    +
    +const used = await graph.users.getById("userId").insights.used()
    +
    +

    Get a Used document by Id

    +

    Using the getById method to get a used document by Id.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const usedDoc = await graph.me.insights.used.getById('Id')()
    +
    +const usedDoc = await graph.users.getById("userId").insights.used.getById('Id')()
    +
    +

    Get the resource from Used document

    +

    Using the resources method to get the resource from a used document.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const resource = await graph.me.insights.used.getById('Id').resource()
    +
    +const resource = await graph.users.getById("userId").insights.used.getById('Id').resource()
    +
    +

    Get all Shared documents

    +

    Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const shared = await graph.me.insights.shared()
    +
    +const shared = await graph.users.getById("userId").insights.shared()
    +
    +

    Get a Shared document by Id

    +

    Using the getById method to get a shared document by Id.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const sharedDoc = await graph.me.insights.shared.getById('Id')()
    +
    +const sharedDoc = await graph.users.getById("userId").insights.shared.getById('Id')()
    +
    +

    Get the resource from a Shared document

    +

    Using the resources method to get the resource from a shared document.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/insights";
    +import "@pnp/graph/users";
    +
    +const resource = await graph.me.insights.shared.getById('Id').resource()
    +
    +const resource = await graph.users.getById("userId").insights.shared.getById('Id').resource()
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/invitations/index.html b/v2/graph/invitations/index.html new file mode 100644 index 000000000..847f749f3 --- /dev/null +++ b/v2/graph/invitations/index.html @@ -0,0 +1,2273 @@ + + + + + + + + + + + + + + + + + + invitations - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/graph/invitations

    +

    The ability invite an external user via the invitation manager

    +

    IInvitations

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { graph } from "@pnp/graph";
    import "@pnp/graph/invitations";
    Preset: Allimport "@pnp/graph/presets/all";
    +

    Create Invitation

    +

    Using the invitations.create() you can create an Invitation. +We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL).

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/invitations"
    +
    +const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/onedrive/index.html b/v2/graph/onedrive/index.html new file mode 100644 index 000000000..9e2f58a5c --- /dev/null +++ b/v2/graph/onedrive/index.html @@ -0,0 +1,2617 @@ + + + + + + + + + + + + + + + + + + onedrive - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/onedrive

    +

    The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can manage drives and drive items in Onedrive.

    +

    IInvitations

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { graph } from "@pnp/graph";
    import "@pnp/graph/onedrive";
    Preset: Allimport "@pnp/graph/presets/all";
    +

    Get the default drive

    +

    Using the drive() you can get the default drive from Onedrive

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives();
    +
    +const drives = await graph.me.drives();
    +
    +
    +

    Get all of the drives

    +

    Using the drives() you can get the users available drives from Onedrive

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives();
    +
    +const drives = await graph.me.drives();
    +
    +
    +

    Get drive by Id

    +

    Using the drives.getById() you can get one of the available drives in Outlook

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId');
    +
    +const drive = await graph.me.drives.getById('driveId');
    +
    +
    +

    Get the associated list of a drive

    +

    Using the list() you get the associated list

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list();
    +
    +const list = await graph.me.drives.getById('driveId').list();
    +
    +
    +

    Get the recent files

    +

    Using the recent() you get the recent files

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent();
    +
    +const files = await graph.me.drives.getById('driveId').recent();
    +
    +
    +

    Get the files shared with me

    +

    Using the sharedWithMe() you get the files shared with the user

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe();
    +
    +const shared = await graph.me.drives.getById('driveId').sharedWithMe();
    +
    +
    +

    Get the Root folder

    +

    Using the root() you get the root folder

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root();
    +
    +const root = await graph.me.drives.getById('driveId').root();
    +
    +
    +

    Get the Children

    +

    Using the children() you get the children

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children();
    +
    +const rootChildren = await graph.me.drives.getById('driveId').root.children();
    +
    +const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children();
    +
    +const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children();
    +
    +
    +

    Add folder or item

    +

    Using the add you can add a folder or an item

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +import { DriveItem as IDriveItem } from "@microsoft/microsoft-graph-types";
    +
    +const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', <IDriveItem>{folder: {}});
    +
    +const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', <IDriveItem>{folder: {}});
    +
    +
    +

    Search items

    +

    Using the search() you can search for items, and optionally select properties

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText')();
    +
    +const search = await graph.me.drives.getById('driveId')root.search('queryText')();
    +
    +
    +

    Get specific item in drive

    +

    Using the items.getById() you can get a specific item from the current drive

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId');
    +
    +const item = await graph.me.drives.getById('driveId').items.getById('itemId');
    +
    +
    +

    Get thumbnails

    +

    Using the thumbnails() you get the thumbnails

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails();
    +
    +const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails();
    +
    +
    +

    Delete drive item

    +

    Using the delete() you delete the current item

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete();
    +
    +const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete();
    +
    +
    +

    Update drive item

    +

    Using the update() you update the current item

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: "New Name"});
    +
    +const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: "New Name"});
    +
    +
    +

    Move drive item

    +

    Using the move() you move the current item, and optionally update it

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/onedrive";
    +
    +// Requires a parentReference to the new folder location
    +const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"});
    +
    +const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: "New Name"});
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/outlook/index.html b/v2/graph/outlook/index.html new file mode 100644 index 000000000..8fc6803cc --- /dev/null +++ b/v2/graph/outlook/index.html @@ -0,0 +1,2363 @@ + + + + + + + + + + + + + + + + + + outlook - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/graph/outlook

    +

    Represents the Outlook services available to a user. Currently, only interacting with categories is supported.

    +

    You can learn more by reading the Official Microsoft Graph Documentation.

    +

    IUsers, IUser, IPeople

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import {Outlook, IOutlook, MasterCategories, IMasterCategories, OutlookCategory, IOutlookCategory} from "@pnp/graph/outlook";
    Selective 2import { graph } from "@pnp/graph";
    import "@pnp/graph/outlook";
    Preset: Allimport { graph, Outlook, IOutlook, MasterCategories, IMasterCategories } from "@pnp/graph/presets/all";
    +

    Get All Categories User

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/outlook";
    +
    +// Delegated permissions
    +const categories = await graph.me.outlook.masterCategories();
    +// Application permissions
    +const categories = await graph.users.getById('{user id}').outlook.masterCategories();
    +
    +

    Add Category User

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/outlook";
    +
    +// Delegated permissions
    +await graph.me.outlook.masterCategories.add({
    +  displayName: 'Newsletters', 
    +  color: 'preset2'
    +});
    +// Application permissions
    +await graph.users.getById('{user id}').outlook.masterCategories.add({
    +  displayName: 'Newsletters', 
    +  color: 'preset2'
    +});
    +
    +

    Update Category

    +

    Known Issue Banner Testing has shown that displayName cannot be updated.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/outlook";
    +import { OutlookCategory } from "@microsoft/microsoft-graph-types";
    +
    +const categoryUpdate: OutlookCategory = {
    +    color: "preset5"
    +}
    +
    +// Delegated permissions
    +const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate);
    +// Application permissions
    +const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);
    +
    +

    Delete Category

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/outlook";
    +
    +// Delegated permissions
    +const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete();
    +// Application permissions
    +const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/photos/index.html b/v2/graph/photos/index.html new file mode 100644 index 000000000..f8868620e --- /dev/null +++ b/v2/graph/photos/index.html @@ -0,0 +1,2351 @@ + + + + + + + + + + + + + + + + + + photos - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/graph/photos

    +

    A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64.

    +

    You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.

    +

    IPhoto

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import {IPhoto, Photo} from "@pnp/graph/photos";
    Selective 2import { graph } from "@pnp/graph";
    import "@pnp/graph/photos";
    Preset: Allimport { graph, IPhoto, Photo } from "@pnp/sp/presets/all";
    +

    Current User Photo

    +

    This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/photos";
    +
    +const photoValue = await graph.me.photo.getBlob();
    +const url = window.URL || window.webkitURL;
    +const blobUrl = url.createObjectURL(photoValue);
    +document.getElementById("photoElement").setAttribute("src", blobUrl);
    +
    +

    Current Group Photo

    +

    This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/groups";
    +import "@pnp/graph/photos";
    +
    +const photoValue = await graph.groups.getById("7d2b9355-0891-47d3-84c8-bf2cd9c62177").photo.getBlob();
    +const url = window.URL || window.webkitURL;
    +const blobUrl = url.createObjectURL(photoValue);
    +document.getElementById("photoElement").setAttribute("src", blobUrl);
    +
    +

    Set User Photo

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/photos";
    +
    +const input = <HTMLInputElement>document.getElementById("thefileinput");
    +const file = input.files[0];
    +await graph.me.photo.setContent(file);
    +
    +

    Set Group Photo

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/photos";
    +
    +const input = <HTMLInputElement>document.getElementById("thefileinput");
    +const file = input.files[0];
    +await graph.me.photo.setContent(file);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/planner/index.html b/v2/graph/planner/index.html new file mode 100644 index 000000000..07b933a07 --- /dev/null +++ b/v2/graph/planner/index.html @@ -0,0 +1,2628 @@ + + + + + + + + + + + + + + + + + + planner - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/planner

    +

    The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described +you can add, update and delete items in Planner.

    +

    IInvitations

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { graph } from "@pnp/graph";
    import "@pnp/graph/planner";
    Preset: Allimport "@pnp/graph/presets/all";
    +

    Get Plans by Id

    +

    Using the planner.plans.getById() you can get a specific Plan. +Planner.plans is not an available endpoint, you need to get a specific Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const plan = await graph.planner.plans.getById('planId')();
    +
    +
    +

    Add new Plan

    +

    Using the planner.plans.add() you can create a new Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const newPlan = await graph.planner.plans.add('groupObjectId', 'title');
    +
    +
    +

    Get Tasks in Plan

    +

    Using the tasks() you can get the Tasks in a Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const planTasks = await graph.planner.plans.getById('planId').tasks();
    +
    +
    +

    Get Buckets in Plan

    +

    Using the buckets() you can get the Buckets in a Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const planBuckets = await graph.planner.plans.getById('planId').buckets();
    +
    +
    +

    Get Details in Plan

    +

    Using the details() you can get the details in a Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const planDetails = await graph.planner.plans.getById('planId').details();
    +
    +
    +

    Delete Plan

    +

    Using the delete() you can get delete a Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const delPlan = await graph.planner.plans.getById('planId').delete('planEtag');
    +
    +
    +

    Update Plan

    +

    Using the update() you can get update a Plan.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});
    +
    +
    +

    Get Task by Id

    +

    Using the planner.tasks.getById() you can get a specific Task. +Planner.tasks is not an available endpoint, you need to get a specific Task.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const task = await graph.planner.tasks.getById('taskId')();
    +
    +
    +

    Add new Task

    +

    Using the planner.tasks.add() you can create a new Task.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const newTask = await graph.planner.tasks.add('planId', 'title');
    +
    +
    +

    Get Details in Task

    +

    Using the details() you can get the details in a Task.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const taskDetails = await graph.planner.tasks.getById('taskId').details();
    +
    +
    +

    Delete Task

    +

    Using the delete() you can get delete a Task.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');
    +
    +
    +

    Update Task

    +

    Using the update() you can get update a Task.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});
    +
    +
    +

    Get Buckets by Id

    +

    Using the planner.buckets.getById() you can get a specific Bucket. +planner.buckets is not an available endpoint, you need to get a specific Bucket.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const bucket = await graph.planner.buckets.getById('bucketId')();
    +
    +
    +

    Add new Bucket

    +

    Using the planner.buckets.add() you can create a new Bucket.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const newBucket = await graph.planner.buckets.add('name', 'planId');
    +
    +
    +

    Update Bucket

    +

    Using the update() you can get update a Bucket.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const updBucket = await graph.planner.buckets.getById('bucketId').update({name: "Name", eTag:'bucketEtag'});
    +
    +
    +

    Delete Bucket

    +

    Using the delete() you can get delete a Bucket.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');
    +
    +
    +

    Get Bucket Tasks

    +

    Using the tasks() you can get Tasks in a Bucket.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/planner"
    +
    +const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/search/index.html b/v2/graph/search/index.html new file mode 100644 index 000000000..04968637f --- /dev/null +++ b/v2/graph/search/index.html @@ -0,0 +1,2264 @@ + + + + + + + + + + + + + + + + + + search - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/graph/search

    +

    The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below.

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { graph } from "@pnp/graph";
    import "@pnp/graph/search";
    Preset: Allimport "@pnp/graph/presets/all";
    +

    Call graph.query

    +

    This example shows calling the search API via the query method of the root graph object.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/search";
    +
    +const results = await graph.query({
    +    entityTypes: ["site"],
    +    query: {
    +        queryString: "test"
    +    },
    +});
    +
    +
    +

    Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.

    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/subscriptions/index.html b/v2/graph/subscriptions/index.html new file mode 100644 index 000000000..1d189093b --- /dev/null +++ b/v2/graph/subscriptions/index.html @@ -0,0 +1,2333 @@ + + + + + + + + + + + + + + + + + + subscriptions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/graph/subscriptions

    +

    The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources:

    +
      +
    • Mail, events, and contacts from Outlook.
    • +
    • Conversations from Office Groups.
    • +
    • Drive root items from OneDrive.
    • +
    • Users and Groups from Azure Active Directory.
    • +
    • Alerts from the Microsoft Graph Security API.
    • +
    +

    Get all of the Subscriptions

    +

    Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/subscriptions"
    +
    +const subscriptions = await graph.subscriptions();
    +
    +
    +

    Create a new Subscription

    +

    Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/subscriptions"
    +
    +const addedSubscription = await graph.subscriptions.add("created,updated", "https://webhook.azurewebsites.net/api/send/myNotifyClient", "me/mailFolders('Inbox')/messages", "2019-11-20T18:23:45.9356913Z");
    +
    +
    +

    Get Subscription by Id

    +

    Using the subscriptions.getById() you can get one of the subscriptions

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/subscriptions"
    +
    +const subscription = await graph.subscriptions.getById('subscriptionId')();
    +
    +
    +

    Delete a Subscription

    +

    Using the subscriptions.getById().delete() you can remove one of the Subscriptions

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/subscriptions"
    +
    +const delSubscription = await graph.subscriptions.getById('subscriptionId').delete();
    +
    +
    +

    Update a Subscription

    +

    Using the subscriptions.getById().update() you can update one of the Subscriptions

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/subscriptions"
    +
    +const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: "created,updated,deleted" });
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/teams/index.html b/v2/graph/teams/index.html new file mode 100644 index 000000000..2d96fae1e --- /dev/null +++ b/v2/graph/teams/index.html @@ -0,0 +1,2619 @@ + + + + + + + + + + + + + + + + + + teams - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/teams

    +

    The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described +you can add, update and delete items in Teams.

    +

    Teams the user is a member of

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users"
    +import "@pnp/graph/teams"
    +
    +const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams();
    +
    +const myJoinedTeams = await graph.me.joinedTeams();
    +
    +
    +

    Get Teams by Id

    +

    Using the teams.getById() you can get a specific Team.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();
    +
    +

    Create new Team/Group - Method #1

    +

    The first way to create a new Team and corresponding Group is to first create the group and then create the team. +Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.

    +

    Create a Team via a specific group

    +

    Here we get the group via id and use createTeam

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +import "@pnp/graph/groups"
    +
    +const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({
    +"memberSettings": {
    +    "allowCreateUpdateChannels": true
    +},
    +"messagingSettings": {
    +        "allowUserEditMessages": true,
    +"allowUserDeleteMessages": true
    +},
    +"funSettings": {
    +    "allowGiphy": true,
    +    "giphyContentRating": "strict"
    +}});
    +
    +

    Create new Team/Group - Method #2

    +

    The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method.

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const team = {
    +        "template@odata.bind": "https://graph.microsoft.com/v1.0/teamsTemplates('standard')",
    +        "displayName": "PnPJS Test Team",
    +        "description": "PnPJS Test Team’s Description",
    +        "members": [
    +            {
    +                "@odata.type": "#microsoft.graph.aadUserConversationMember",
    +                "roles": ["owner"],
    +                "user@odata.bind": "https://graph.microsoft.com/v1.0/users('{owners user id}')",
    +            },
    +        ],
    +    };
    +
    +const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team);
    +//To check the status of the team creation, call getOperationById for the newly created team.
    +const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);
    +
    +

    Clone a Team

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
    +'Cloned','description','apps,tabs,settings,channels,members','public');
    +
    +
    +

    Get Teams Async Operation

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam(
    +'Cloned','description','apps,tabs,settings,channels,members','public');
    +const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);
    +
    +

    Archive a Team

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();
    +
    +

    Unarchive a Team

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();
    +
    +

    Get all channels of a Team

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();
    +
    +

    Get channel by Id

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();
    +
    +
    +

    Create a new Channel

    +
    import { graph } from "@pnp/graph";
    +
    +const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');
    +
    +
    +

    Get installed Apps

    +
    import { graph } from "@pnp/graph";
    +
    +const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();
    +
    +
    +

    Add an App

    +
    import { graph } from "@pnp/graph";
    +
    +const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');
    +
    +
    +

    Remove an App

    +
    import { graph } from "@pnp/graph";
    +
    +const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove();
    +
    +
    +

    Get Tabs from a Channel

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
    +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();
    +
    +
    +

    Get Tab by Id

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
    +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();
    +
    +
    +

    Add a new Tab

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/teams"
    +
    +const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').
    +channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',<TabsConfiguration>{});
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/graph/users/index.html b/v2/graph/users/index.html new file mode 100644 index 000000000..275ad0991 --- /dev/null +++ b/v2/graph/users/index.html @@ -0,0 +1,2461 @@ + + + + + + + + + + + + + + + + + + users - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/graph/users

    +

    Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services.

    +

    You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation.

    +

    IUsers, IUser, IPeople

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { graph } from "@pnp/graph";
    import {IUser, IUsers, User, Users, IPeople, People} from "@pnp/graph/users";
    Selective 2import { graph } from "@pnp/graph";
    import "@pnp/graph/users";
    Preset: Allimport { graph,IUser, IUsers, User, Users, IPeople, People } from "@pnp/graph/presets/all";
    +

    Current User

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const currentUser = await graph.me();
    +
    +

    Get All Users in the Organization

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const allUsers = await graph.users();
    +
    +

    Get a User by email address (or user id)

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const matchingUser = await graph.users.getById('jane@contoso.com')();
    +
    +

    Update Current User

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +await graph.me.update({
    +    displayName: 'John Doe'
    +});
    +
    +

    People

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const people = await graph.me.people();
    +
    +// get the top 3 people
    +const people = await graph.me.people.top(3)();
    +
    +

    People

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const people = await graph.me.people();
    +
    +// get the top 3 people
    +const people = await graph.me.people.top(3)();
    +
    +

    Manager

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const manager = await graph.me.manager();
    +
    +

    Direct Reports

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +
    +const reports = await graph.me.directReports();
    +
    +

    Photo

    +
    import { graph } from "@pnp/graph";
    +import "@pnp/graph/users";
    +import "@pnp/graph/photos";
    +
    +const currentUser = await graph.me.photo();
    +const specificUser = await graph.users.getById('jane@contoso.com').photo();
    +
    +

    User Photo Operations

    +

    See Photos

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/img/ConsoleListenerColors.png b/v2/img/ConsoleListenerColors.png new file mode 100644 index 000000000..0b07afbf7 Binary files /dev/null and b/v2/img/ConsoleListenerColors.png differ diff --git a/v2/img/Logo.png b/v2/img/Logo.png new file mode 100644 index 000000000..73dc570b3 Binary files /dev/null and b/v2/img/Logo.png differ diff --git a/v2/img/PnPJS_FluentAPI.gif b/v2/img/PnPJS_FluentAPI.gif new file mode 100644 index 000000000..e65e84fba Binary files /dev/null and b/v2/img/PnPJS_FluentAPI.gif differ diff --git a/v2/img/SPFx-On-Premesis-2016-1.png b/v2/img/SPFx-On-Premesis-2016-1.png new file mode 100644 index 000000000..9ea42e8dd Binary files /dev/null and b/v2/img/SPFx-On-Premesis-2016-1.png differ diff --git a/v2/img/office365-header-icon.png b/v2/img/office365-header-icon.png new file mode 100644 index 000000000..529191ea6 Binary files /dev/null and b/v2/img/office365-header-icon.png differ diff --git a/v2/img/usage-2020-eoy.png b/v2/img/usage-2020-eoy.png new file mode 100644 index 000000000..3f89f8022 Binary files /dev/null and b/v2/img/usage-2020-eoy.png differ diff --git a/v2/index.html b/v2/index.html new file mode 100644 index 000000000..4f6e2f2ac --- /dev/null +++ b/v2/index.html @@ -0,0 +1,2410 @@ + + + + + + + + + + + + + + + + + + PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Home

    + +

    SharePoint Patterns and Practices Logo

    +

    PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community.

    +

    Fluent API in action

    +

    Animation of the library in use, note intellisense help in building your queries

    +

    General Guidance

    +

    These articles provide general guidance for working with the libraries. If you are migrating from v1 please review the transition guide.

    + +

    Packages

    +

    Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope.

    +

    The latest published version is npm version.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @pnp/
    adaljsclientProvides an adaljs wrapper suitable for use with PnPjs
    commonProvides shared functionality across all pnp libraries
    config-storeProvides a way to manage configuration within your application
    graphProvides a fluent api for working with Microsoft Graph
    loggingLight-weight, subscribable logging framework
    msaljsclientProvides an msal wrapper suitable for use with PnPjs
    nodejsProvides functionality enabling the @pnp libraries within nodejs
    odataProvides shared odata functionality and base classes
    spProvides a fluent api for working with SharePoint REST
    sp-addinhelpersProvides functionality for working within SharePoint add-ins
    +

    Authentication

    +

    We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!

    +

    Issues, Questions, Ideas

    +

    Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.

    +

    Changelog

    +

    Please review the CHANGELOG for release details on all library changes.

    +

    Code of Conduct

    +

    This project has adopted the Microsoft Open Source Code of Conduct. For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.

    +

    "Sharing is Caring"

    +

    Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program.

    +

    Disclaimer

    +

    THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/logging/index.html b/v2/logging/index.html new file mode 100644 index 000000000..d22a640d8 --- /dev/null +++ b/v2/logging/index.html @@ -0,0 +1,2622 @@ + + + + + + + + + + + + + + + + + + logging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/logging

    +

    npm version

    +

    The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.

    +

    Getting Started

    +

    Install the logging module, it has no other dependencies

    +

    npm install @pnp/logging --save

    +

    Understanding the Logging Framework

    +

    The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter.

    +
    /**
    + * Interface that defines a log listener
    + *
    + */
    +export interface ILogListener {
    +    /**
    +     * Any associated data that a given logging listener may choose to log or ignore
    +     *
    +     * @param entry The information to be logged
    +     */
    +    log(entry: ILogEntry): void;
    +}
    +
    +/**
    + * Interface that defines a log entry
    + *
    + */
    +export interface ILogEntry {
    +    /**
    +     * The main message to be logged
    +     */
    +    message: string;
    +    /**
    +     * The level of information this message represents
    +     */
    +    level: LogLevel;
    +    /**
    +     * Any associated data that a given logging listener may choose to log or ignore
    +     */
    +    data?: any;
    +}
    +
    +

    Log Levels

    +
    export const enum LogLevel {
    +    Verbose = 0,
    +    Info = 1,
    +    Warning = 2,
    +    Error = 3,
    +    Off = 99,
    +}
    +
    +

    Writing to the Logger

    +

    To write information to a logger you can use either write, writeJSON, or log.

    +
    import {
    +    Logger,
    +    LogLevel
    +} from "@pnp/logging";
    +
    +// write logs a simple string as the message value of the LogEntry
    +Logger.write("This is logging a simple string");
    +
    +// optionally passing a level, default level is Verbose
    +Logger.write("This is logging a simple string", LogLevel.Error);
    +
    +// this will convert the object to a string using JSON.stringify and set the message with the result
    +Logger.writeJSON({ name: "value", name2: "value2"});
    +
    +// optionally passing a level, default level is Verbose
    +Logger.writeJSON({ name: "value", name2: "value2"}, LogLevel.Warning);
    +
    +// specify the entire LogEntry interface using log
    +Logger.log({
    +    data: { name: "value", name2: "value2"},
    +    level: LogLevel.Warning,
    +    message: "This is my message"
    +});
    +
    +

    Log an error

    +

    There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error +instance passed in, the level will be 'Error', and the message will be the Error instance's message property.

    +
    const e = Error("An Error");
    +
    +Logger.error(e);
    +
    +

    Subscribing a Listener

    +

    By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request.

    +
    import {
    +    Logger,
    +    ConsoleListener,
    +    LogLevel
    +} from "@pnp/logging";
    +
    +// subscribe a listener
    +Logger.subscribe(new ConsoleListener());
    +
    +// set the active log level
    +Logger.activeLogLevel = LogLevel.Info;
    +
    +

    Available Listeners

    +

    There are two listeners included in the library, ConsoleListener and FunctionListener.

    +

    ConsoleListener

    +

    This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.

    +

    Configuration Options

    +

    Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).

    +
    Using a Prefix
    +

    To add a prefix to all output, supply a string in the constructor:

    +
    import {
    +    Logger,
    +    ConsoleListener,
    +    LogLevel
    +} from "@pnp/logging";
    +
    +const LOG_SOURCE: string = 'MyAwesomeWebPart';
    +Logger.subscribe(new ConsoleListener(LOG_SOURCE));
    +Logger.activeLogLevel = LogLevel.Info;
    +
    +

    With the above configuration, Logger.write("My special message"); will be output to the console as:

    +
    MyAwesomeWebPart - My special message
    +
    +
    Customizing Text Color
    +

    You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color).

    +

    Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.):

    +
    import {
    +    Logger,
    +    ConsoleListener,
    +    LogLevel
    +} from "@pnp/logging";
    +
    +const LOG_SOURCE: string = 'MyAwesomeWebPart';
    +Logger.subscribe(new ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'}));
    +Logger.activeLogLevel = LogLevel.Info;
    +
    +

    With the above configuration:

    +
    Logger.write("My special message");
    +Logger.write("A warning!", LogLevel.Warning);
    +
    +

    Will result in messages that look like this:

    +

    ConsoleListener with Colors

    +

    Color options:

    +
      +
    • color: Default text color for all logging levels unless they're specified
    • +
    • verboseColor: Text color to use for messages with LogLevel.Verbose
    • +
    • infoColor: Text color to use for messages with LogLevel.Info
    • +
    • warningColor: Text color to use for messages with LogLevel.Warning
    • +
    • errorColor: Text color to use for messages with LogLevel.Error
    • +
    +

    To set colors without a prefix, specify either undefined or an empty string for the first parameter:

    +
    Logger.subscribe(new ConsoleListener(undefined, {color:'purple'}));
    +
    +

    FunctionListener

    +

    The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages.

    +
    import {
    +    Logger,
    +    FunctionListener,
    +    ILogEntry
    +} from "@pnp/logging";
    +
    +let listener = new FunctionListener((entry: ILogEntry) => {
    +
    +    // pass all logging data to an existing framework
    +    MyExistingCompanyLoggingFramework.log(entry.message);
    +});
    +
    +Logger.subscribe(listener);
    +
    +

    Create a Custom Listener

    +

    If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface.

    +
    import {
    +    Logger,
    +    ILogListener,
    +    ILogEntry
    +} from "@pnp/logging";
    +
    +class MyListener implements ILogListener {
    +
    +    log(entry: ILogEntry): void {
    +        // here you would do something with the entry
    +    }
    +}
    +
    +Logger.subscribe(new MyListener());
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/news/2020-year-in-review/index.html b/v2/news/2020-year-in-review/index.html new file mode 100644 index 000000000..cce127fa3 --- /dev/null +++ b/v2/news/2020-year-in-review/index.html @@ -0,0 +1,2525 @@ + + + + + + + + + + + + + + + + + + 2020 Year In Review - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    2020 Year End Report

    +

    Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year.

    +

    This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules.

    +

    We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community.

    +

    Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured "roots" such as "sp" or "graph" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios.

    +

    Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.

    +

    Usage

    +

    In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227.

    +

    These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November.

    +

    Graph showing requests and tenants/month for @pnp/sp

    +
    +

    1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds

    +
    +

    Releases

    +

    We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log, updated with each release. You can check our scheduled releases through project milestones, understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.

    +

    NPM Package download statistics (@pnp/sp):

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    MonthCount*MonthCount
    January100,686*July36,805
    February34,437*August38,897
    March34,574*September45,968
    April32,436*October46,655
    May34,482*November45,511
    June34,408*December58,977
    Grand Total543,836
    +

    With 2020 our total all time downloads of @pnp/sp is now at: 949,638

    +
    +

    Stats from https://npm-stat.com/

    +
    +

    Future Plans

    +

    Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date.

    +

    Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.

    +

    New Lead Maintainer

    +

    With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work.

    +

    Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean.

    +

    We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.

    +

    Contributors

    +

    As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.

    +

    + AJIXuMuK + + Ashikpaul + + cesarhoeflich + + dcashpeterson + + dependabot[bot] + + derhallim + + DRamalho92 + + f1nzer + + Harshagracy + + holylander + + hugoabernier + + JakeStanger + + jaywellings + + JMTeamway + + joelfmrodrigues + + juliemturner + + jusper-dk + + KEMiCZA + + koltyakov + + kunj-sangani + + MarkyDeParky + + mikezimm + + mrebuffet + + naugtur + + NZainchkovskiy + + PaoloPia + + patrick-rodgers + + ravichandran-blog + + RoelVB + + siddharth-vaghasia + + simonagren + + tavikukko + + ValerasNarbutas +

    +

    Sponsors

    +

    We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs.

    +

    Thank You

    +

    + KEMiCZA + + Sympraxis Consulting + + thechriskent + + erwinvanhunen + + PopWarner + + VesaJuvonen + + LauraKokkarinen + + ricardocarneiro + + andrewconnell +

    +

    Closing

    +

    In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program.

    +

    Wishing you the very best for 2021,

    +

    The PnPjs Team

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs-support/index.html b/v2/nodejs-support/index.html new file mode 100644 index 000000000..c7c459630 --- /dev/null +++ b/v2/nodejs-support/index.html @@ -0,0 +1,2398 @@ + + + + + + + + + + + + + + + + + + Working in Nodejs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Working in Nodejs

    +

    As outlined on the getting started page you can easily use the library with Nodejs, but there are some key differences you need to consider.

    +

    But first a little history, you can skip this part if you just want to see how things work but we felt some folks might be interested. To make selective imports work we need to support es module syntax for client-side environments such as SPFx development. All versions of Nodejs that are currently LTS do not support es modules without flags (as of when this was written). We thought we had a scheme to handle this following the available guidance but ultimately it didn't work across all node versions and we unpublished 2.0.1.

    +

    CommonJS Libraries

    +

    Because of the difficulties of working with es modules in node we recommend using our mirror packages providing commonjs modules. These can be installed by using the package name and appending -commonjs, such as:

    +
    npm install @pnp/sp-commonjs @pnp/nodejs-commonjs
    +
    +

    These packages are built from the same source and released at the same time so all updates are included with each release. The only difference is that for the sp-commonjs and graph-commonjs packages we target the "all" preset as the entry point. This makes things a little easier in node where bundle sizes aren't an issue. You can see this in the nodejs-app sample. Here is that sample explained fully:

    +

    Install Libraries

    +

    We want to make a simple request to SharePoint so we need to first install the modules we need:

    +
    npm install @pnp/sp-commonjs @pnp/nodejs-commonjs --save
    +
    +

    We will also install TypeScript:

    +
    npm install typescript --save-dev
    +
    +

    index.ts

    +

    We will create an index.ts file and add the following code. You will need to update the site url, client id, and client secret to your values. This should be done using a settings file or something like Azure KeyVault for production, but for this example it is good enough.

    +
    // our imports come from the -commonjs libs
    +import { SPFetchClient } from "@pnp/nodejs-commonjs";
    +import { sp } from "@pnp/sp-commonjs";
    +
    +// we call setup to use the node client
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{ site url }", "{ client id }", "{ client secret }");
    +        },
    +    },
    +});
    +
    +async function makeRequest() {
    +
    +    // make a request to get the web's details
    +    const w = await sp.web();
    +    console.log(JSON.stringify(w, null, 2));
    +}
    +
    +// get past no await at root of app
    +makeRequest();
    +
    +
    +

    Don't forget you will need to register an app to get the client id and secret.

    +
    +

    Add a tsconfig.json

    +

    Not strictly necessary but very useful to include a tsconfig.json to control how tsc transpiles your code to JavaScript

    +
    {
    +    "compilerOptions": {
    +        "module": "commonjs",
    +        "target": "esnext",
    +        "moduleResolution": "node",
    +        "declaration": true,
    +        "outDir": "dist",
    +        "skipLibCheck": true,
    +        "sourceMap": true,
    +        "lib": [
    +            "dom",
    +            "esnext"
    +        ]
    +    },
    +    "files": [
    +        "./index.ts"
    +    ]
    +}
    +
    +

    Add an script to package.json

    +

    We add the "start" script to the default package.json

    +
    {
    +  "name": "nodejs-app",
    +  "version": "1.0.0",
    +  "description": "Sample nodejs app using PnPjs",
    +  "main": "index.js",
    +  "scripts": {
    +    "start": "tsc -p . && node dist/index.js",
    +    "test": "echo \"Error: no test specified\" && exit 1"
    +  },
    +  "author": "",
    +  "license": "MIT",
    +  "dependencies": {
    +    "@pnp/nodejs-commonjs": "^2.0.2-5",
    +    "@pnp/sp-commonjs": "^2.0.2-5"
    +  },
    +  "devDependencies": {
    +    "typescript": "^3.7.5"
    +  }
    +}
    +
    +

    Run It

    +

    You can now run your program using:

    +
    npm start
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/adal-fetch-client/index.html b/v2/nodejs/adal-fetch-client/index.html new file mode 100644 index 000000000..5b111c8b1 --- /dev/null +++ b/v2/nodejs/adal-fetch-client/index.html @@ -0,0 +1,2204 @@ + + + + + + + + + + + + + + + + + + AdalFetchClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs/adalfetchclient

    +

    The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below +outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected.

    +
    import { AdalFetchClient } from "@pnp/nodejs";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +// setup the client using graph setup function
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +            return new AdalFetchClient("{tenant}", "{app id}", "{app secret}");
    +        },
    +    },
    +});
    +
    +// execute a library request as normal
    +const g = await graph.groups();
    +
    +console.log(JSON.stringify(g, null, 4));
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/bearer-token-fetch-client/index.html b/v2/nodejs/bearer-token-fetch-client/index.html new file mode 100644 index 000000000..537d7da08 --- /dev/null +++ b/v2/nodejs/bearer-token-fetch-client/index.html @@ -0,0 +1,2203 @@ + + + + + + + + + + + + + + + + + + BearerTokenFetchClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs/BearerTokenFetchClient

    +

    The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you.

    +
    import { BearerTokenFetchClient } from "@pnp/nodejs";
    +import { graph } from "@pnp/graph/presets/all";
    +
    +// setup the client using graph setup function
    +graph.setup({
    +    graph: {
    +        fetchClientFactory: () => {
    +            return new BearerTokenFetchClient("{Bearer Token}");
    +        },
    +    },
    +});
    +
    +// execute a library request as normal
    +const g = await graph.groups();
    +
    +console.log(JSON.stringify(g, null, 4));
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/index.html b/v2/nodejs/index.html new file mode 100644 index 000000000..10c75220c --- /dev/null +++ b/v2/nodejs/index.html @@ -0,0 +1,2261 @@ + + + + + + + + + + + + + + + + + + nodejs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs

    +

    npm version

    +

    This package supplies helper code when using the @pnp libraries within the context of nodejs. Primarily these consist of clients to enable use of the libraries in nodejs.

    +

    Getting Started

    +

    Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the +exported functionality.

    +

    npm install @pnp/sp @pnp/nodejs --save

    + +

    SP Extensions

    +

    Added in 2.0.9

    +

    A set of nodejs specific extensions for the @pnp/sp library.

    + + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/provider-hosted-app/index.html b/v2/nodejs/provider-hosted-app/index.html new file mode 100644 index 000000000..61170385b --- /dev/null +++ b/v2/nodejs/provider-hosted-app/index.html @@ -0,0 +1,2220 @@ + + + + + + + + + + + + + + + + + + ProviderHostedRequestContext - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs/providerhostedrequestcontext

    +

    The ProviderHostedRequestContext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user.

    +

    The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI.

    +

    Note: To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context.

    +
    import { sp, SPRest } from "@pnp/sp/presets/all";
    +import { NodeFetchClient, ProviderHostedRequestContext } from "@pnp/nodejs";
    +
    +// configure your node options
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new NodeFetchClient();
    +        },
    +    },
    +});
    +
    +// get request data generated by /_layouts/15/AppRedirect.aspx
    +const spAppToken = request.body.SPAppToken;
    +const spSiteUrl = request.body.SPSiteUrl;
    +
    +// create a context based on the add-in details and SPAppToken
    +const ctx = await ProviderHostedRequestContext.create(spSiteUrl, "{client id}", "{client secret}", spAppToken);
    +
    +// create an SPRest object configured to use our context
    +// this is used in place of the global sp object
    +const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl);
    +const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl);
    +
    +// make a request on behalf of the user
    +const user = await userSP.web.currentUser();
    +console.log(`Hello ${user.Title}`);
    +
    +// make an add-in only request
    +const app = await addinSP.web.currentUser();
    +console.log(`Add-in principal: ${app.Title}`);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/proxy/index.html b/v2/nodejs/proxy/index.html new file mode 100644 index 000000000..1184999ee --- /dev/null +++ b/v2/nodejs/proxy/index.html @@ -0,0 +1,2238 @@ + + + + + + + + + + + + + + + + + + proxy - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs/proxy

    +

    In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler.

    +

    setProxyUrl

    +

    Basic Usage

    +

    You need to import the setProxyUrl function from @pnp/nodejs library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library.

    +
    import { SPFetchClient, SPOAuthEnv, setProxyUrl } from "@pnp/nodejs";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +
    +            // call the set proxy url function and it will be used for all requests regardless of client
    +            setProxyUrl("{your proxy url}");
    +            return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO);
    +        },
    +    },
    +});
    +
    +

    Use with Fiddler

    +

    To get Fiddler to work you may need to set an environment variable. This should only be done for testing!

    +
    import { SPFetchClient, SPOAuthEnv, setProxyUrl } from "@pnp/nodejs";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +
    +            // ignore certificate errors: ONLY FOR TESTING!!
    +            process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
    +
    +            // this is my fiddler url locally
    +            setProxyUrl("http://127.0.0.1:8888");
    +            return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO);
    +        },
    +    },
    +});
    +
    +

    setProxyAgent

    +

    Added in 2.0.11

    +

    You need to import the setProxyAgent function from @pnp/nodejs library and call it with your proxy url. You can supply any valid proxy and it will be used.

    +
    import { SPFetchClient, SPOAuthEnv, setProxyAgent } from "@pnp/nodejs";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +
    +            const myAgent = new MyAgentOfSomeType({});
    +
    +            // call the set proxy agent function and it will be used for all requests regardless of client
    +            setProxyAgent(myAgent);
    +            return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO);
    +        },
    +    },
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/sp-extensions/index.html b/v2/nodejs/sp-extensions/index.html new file mode 100644 index 000000000..ba352ffd1 --- /dev/null +++ b/v2/nodejs/sp-extensions/index.html @@ -0,0 +1,2362 @@ + + + + + + + + + + + + + + + + + + sp Extensions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs - sp extensions

    +

    By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api. This article describes them.

    +
    +

    These examples use the *-commonjs version of the libraries as they target node, you can read more about the differences.

    +
    +

    IFile.getStream

    +

    Allows you to read a response body as a nodejs PassThrough stream.

    +
    // by importing the the library the node specific extensions are automatically applied
    +import { SPFetchClient, SPNS } from "@pnp/nodejs-commonjs";
    +import { sp } from "@pnp/sp-commonjs";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{url}", "{id}", "{secret}");
    +        },
    +    },
    +});
    +
    +// get the stream
    +const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl("/sites/dev/file.txt").getStream();
    +
    +// see if we have a known length
    +console.log(streamResult.knownLength);
    +
    +// read the stream
    +// this is a very basic example - you can do tons more with streams in node
    +const txt = await new Promise<string>((resolve) => {
    +    let data = "";
    +    stream.body.on("data", (chunk) => data += chunk);
    +    stream.body.on("end", () => resolve(data));
    +});
    +
    +

    IFiles.addChunked

    +

    Added in 2.1.0

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders/web";
    +import "@pnp/sp/folders/list";
    +import "@pnp/sp/files/web";
    +import "@pnp/sp/files/folder";
    +import * as fs from "fs";
    +
    +
    +const stream = fs.createReadStream("{file path}");
    +const files = sp.web.defaultDocumentLibrary.rootFolder.files;
    +
    +await files.addChunked(name, stream, null, true, 10);
    +
    +

    IFile.setStreamContentChunked

    +

    Added in 2.1.0

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders/web";
    +import "@pnp/sp/folders/list";
    +import "@pnp/sp/files/web";
    +import "@pnp/sp/files/folder";
    +import * as fs from "fs";
    +
    +const stream = fs.createReadStream("{file path}");
    +const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName("file-name.txt");
    +
    +await file.setStreamContentChunked(stream);
    +
    +

    Explicit import

    +

    If you don't need to import anything from the library, but would like to include the extensions just import the library as shown.

    +
    // ES Modules:  import "@pnp/nodejs";
    +import "@pnp/nodejs-commonjs";
    +
    +// get the stream
    +const streamResult = await sp.web.getFileByServerRelativeUrl("/sites/dev/file.txt").getStream();
    +
    +

    Accessing SP Extension Namespace

    +

    There are classes and interfaces included in extension modules, which you can access through a namespace, "SPNS".

    +
    import { SPNS } from "@pnp/nodejs-commonjs";
    +
    +const parser = new SPNS.StreamParser();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/nodejs/sp-fetch-client/index.html b/v2/nodejs/sp-fetch-client/index.html new file mode 100644 index 000000000..4a58274fb --- /dev/null +++ b/v2/nodejs/sp-fetch-client/index.html @@ -0,0 +1,2294 @@ + + + + + + + + + + + + + + + + + + SPFetchClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/nodejs/spfetchclient

    +

    The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively.

    +

    See: How to register a legacy SharePoint application

    +
    import { SPFetchClient } from "@pnp/nodejs";
    +import { sp } from "@pnp/sp/presets/all";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{site url}", "{client id}", "{client secret}");
    +        },
    +    },
    +});
    +
    +// execute a library request as normal
    +const w = await sp.web();
    +
    +console.log(JSON.stringify(w, null, 4));
    +
    +

    Set Authentication Environment

    +

    For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK.

    +
      +
    • SPO : (default) for all *.sharepoint.com urls
    • +
    • China: for China hosted cloud
    • +
    • Germany: for Germany local cloud
    • +
    • USDef: USA Defense cloud
    • +
    • USGov: USA Government cloud
    • +
    +
    import { sp } from "@pnp/sp/presets/all";
    +import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.China);
    +        },
    +    },
    +});
    +
    +

    Set Realm

    +

    In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx and copying the GUID value that appears after the "@" - this is the realm id.

    +
    import { sp } from "@pnp/sp/presets/all";
    +import { SPFetchClient, SPOAuthEnv } from "@pnp/nodejs";
    +
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{site url}", "{client id}", "{client secret}", SPOAuthEnv.SPO, "{realm}");
    +        },
    +    },
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/npm-scripts/index.html b/v2/npm-scripts/index.html new file mode 100644 index 000000000..6946e972d --- /dev/null +++ b/v2/npm-scripts/index.html @@ -0,0 +1,2521 @@ + + + + + + + + + + + + + + + + + + Npm Scripts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Supported NPM Scripts

    +

    As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies.

    +

    This article outlines the current scripts we've implemented and how to use them, with available options and examples.

    +

    Start

    +

    Executes the serve command

    +
    npm start
    +
    +

    Serve

    +

    Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node.

    +
    npm run serve
    +
    +

    Test

    +

    Runs the tests and coverage for the library.

    +
    +

    Starting with 2.3.0 ONLY MSAL auth is supported for running the tests. More details on setting up MSAL for node.

    +
    +

    Options

    +

    There are several options you can provide to the test command. All of these need to be separated using a "--" double hyphen so they are passed to the spawned sub-commands.

    +

    Test a Single Package

    +
    +

    --package or -p

    +
    +

    This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory.

    +
    # run only sp tests
    +npm test -- -p sp
    +
    +# run only logging tests
    +npm test -- -package logging
    +
    +

    Run a Single Test File

    +
    +

    --single or --s

    +
    +

    You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags.

    +
    # run only sp web tests
    +npm test -- -p sp -s web
    +
    +# run only graph groups tests
    +npm test -- -package graph -single groups
    +
    +

    Specify a Site

    +
    +

    --site

    +
    +

    By default every time you run the tests a new sub-site is created below the site specified in your settings file. You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option.

    +

    This option can be used with any or none of the other testing options.

    +
    # run only sp web tests with a certain site
    +npm test -- -p sp -s web --site https://some.site.com/sites/dev
    +
    +

    Cleanup

    +
    +

    --cleanup

    +
    +

    If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted.

    +
    # clean up our testing site
    +npm test -- --cleanup
    +
    +

    Logging

    +
    +

    --logging

    +
    +

    If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output.

    +
    # enable logging during testing
    +npm test -- --logging
    +
    +

    spVerbose

    +

    Added in 2.0.13

    +
    +

    --spverbose

    +
    +

    This flag will enable "verbose" OData mode for SharePoint tests. This flag is compatible with other flags.

    +
    npm test -- --spverbose
    +
    +

    build

    +

    Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed.

    +
    npm run build
    +
    +

    package

    +

    Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published.

    +
    npm run package
    +
    +

    lint

    +

    Runs the linter.

    +
    npm run lint
    +
    +

    clean

    +

    Removes any generated folders from the working directory.

    +
    npm run clean
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/caching/index.html b/v2/odata/caching/index.html new file mode 100644 index 000000000..8dd7143c4 --- /dev/null +++ b/v2/odata/caching/index.html @@ -0,0 +1,2465 @@ + + + + + + + + + + + + + + + + + + caching - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/queryable/caching

    +

    Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests.

    +

    The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph.

    +

    Basic example

    +

    You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The code below will get items from a list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() method should always be the last method in the chain before the get() (OR if you are using batching these methods can be transposed, more details below).

    +
    import { sp } from "@pnp/sp";
    +
    +const r = await sp.web.lists.getByTitle("Tasks").items.usingCaching()();
    +console.log(r);
    +
    +const r2 = await sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching()();
    +console.log(r2);
    +
    +

    Globally Configure Cache Settings

    +

    If you would not like to use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application.

    +
    import { sp } from "@pnp/sp";
    +
    +sp.setup({
    +    defaultCachingStore: "session", // or "local"
    +    defaultCachingTimeoutSeconds: 30,
    +    globalCacheDisable: false // or true to disable caching in case of debugging/testing
    +});
    +
    +const r = await sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching()();
    +console.log(r);
    +
    +

    Per Call Configuration

    +

    If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key.

    +
    export interface ICachingOptions {
    +    expiration?: Date;
    +    storeName?: "session" | "local";
    +    key: string;
    +}
    +
    +
    import { sp } from "@pnp/sp";
    +import { dateAdd } from "@pnp/core";
    +
    +const r = await sp.web.lists.getByTitle("Tasks").items.top(5).orderBy("Modified").usingCaching({
    +    expiration: dateAdd(new Date(), "minute", 20),
    +    key: "My Key",
    +    storeName: "local"
    +})();
    +console.log(r);
    +
    +

    Using Batching with Caching

    +

    You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid.

    +
    import { sp } from "@pnp/sp";
    +
    +let batch = sp.createBatch();
    +
    +sp.web.lists.inBatch(batch).usingCaching()().then(r => {
    +    console.log(r)
    +});
    +
    +sp.web.lists.getByTitle("Tasks").items.usingCaching().inBatch(batch)().then(r => {
    +    console.log(r)
    +});
    +
    +batch.execute().then(() => console.log("All done!"));
    +
    +

    Implement Custom Caching

    +

    You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here.

    +

    Implement caching helper method

    +

    We create a map to act as our cache storage and a function to wrap the request caching logic

    +
    const map = new Map<string, any>();
    +
    +async function staleWhileRevalidate<T>(key: string, p: Promise<T>): Promise<T> {
    +
    +    if (map.has(key)) {
    +
    +        // In Cache
    +        p.then(u => {
    +            // Update Cache once we have a result
    +            map.set(key, u);
    +        });
    +
    +        // Return from Cache
    +        return map.get(key);
    +    }
    +
    +    // Not In Cache so we need to wait for the value
    +    const r = await p;
    +
    +    // Set Cache
    +    map.set(key, r);
    +
    +    // Return from Promise
    +    return r;
    +}
    +
    +

    Usage

    +
    +

    Don't call usingCaching just apply the helper method

    +
    +
    // this one will wait for the request to finish
    +const r1 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description")());
    +
    +console.log(JSON.stringify(r1, null, 2));
    +
    +// this one will return the result from cache and then update the cache in the background
    +const r2 = await staleWhileRevalidate("test1", sp.web.select("Title", "Description")());
    +
    +console.log(JSON.stringify(r2, null, 2));
    +
    +

    Wrapper Function

    +

    You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well.

    +
    interface WebData {
    +    Title: string;
    +    Description: string;
    +}
    +
    +function getWebData(): Promise<WebData> {
    +
    +    return staleWhileRevalidate("test1", sp.web.select("Title", "Description")());
    +}
    +
    +
    +// this one will wait for the request to finish
    +const r1 = await getWebData();
    +
    +console.log(JSON.stringify(r1, null, 2));
    +
    +// this one will return the result from cache and then update the cache in the background
    +const r2 = await getWebData();
    +
    +console.log(JSON.stringify(r2, null, 2));
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/core/index.html b/v2/odata/core/index.html new file mode 100644 index 000000000..0083c8923 --- /dev/null +++ b/v2/odata/core/index.html @@ -0,0 +1,2322 @@ + + + + + + + + + + + + + + + + + + core - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/queryable/core

    +

    This module contains shared interfaces and abstract classes used within the @pnp/queryable package and those items that inherit from it.

    +

    ProcessHttpClientResponseException

    +

    The exception thrown when a response is returned and cannot be processed.

    +

    interface ODataParser

    +

    Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the +value to be returned. It has two methods, one is optional:

    +
      +
    • parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T
    • +
    • hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor
    • +
    +

    ODataParserBase

    +

    The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods.

    +

    Create a custom parser from ODataParserBase

    +

    You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases.

    +
    class MyParser extends ODataParserBase<any> {
    +
    +    // we need to override the parse method to do our custom stuff
    +    public parse(r: Response): Promise<T> {
    +
    +        // we wrap everything in a promise
    +        return new Promise((resolve, reject) => {
    +
    +            // lets use the default error handling which returns true for no error
    +            // and will call reject with an error if one exists
    +            if (this.handleError(r, reject)) {
    +
    +                // now we add our custom parsing here
    +                r.text().then(txt => {
    +                    // here we call a made up function to parse the result
    +                    // this is where we would do our parsing as required
    +                    myCustomerUnencode(txt).then(v => {
    +                        resolve(v);
    +                    });
    +                });
    +            }
    +        });
    +    }
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/debug/index.html b/v2/odata/debug/index.html new file mode 100644 index 000000000..5cfcbd1d1 --- /dev/null +++ b/v2/odata/debug/index.html @@ -0,0 +1,2358 @@ + + + + + + + + + + + + + + + + + + Debugging Proxy Objects - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Debugging Proxy Objects

    +

    Because all queryables are now represented as Proxy objects you can't immediately see the properties/method of the object or the data stored about the request. In certain debugging scenarios it can help to get visibility into the object that is wrapped by the proxy. To enable this we provide a set of extensions to help.

    +

    The debug extensions are added by including the import "@pnp/queryable/debug"; statement in your project. It should be removed for production. This module provides several methods to help with debugging Queryable Proxy objects.

    +

    Unwrap

    +

    The __unwrap() method returns the concrete Queryable instance wrapped by the Proxy. You can then examine this object in various ways or dump it to the console for debugging.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/queryable/debug";
    +
    +// unwrap the underlying concrete queryable instance
    +const unwrapped = sp.web.__unwrap();
    +
    +console.log(JSON.stringify(unwrapped, null, 2));
    +
    +
    +

    Note: It is not supported to unwrap objects and then use them. It may work in some cases, but this behavior may change as what is contained with the Proxy is an implementation detail and should not be relied upon. Without the Proxy wrapper we make no guarantees.

    +
    +

    Data

    +

    All of the information related to a queryable's request is contained within the "data" property. If you need to grab that information you can use the __data property.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/queryable/debug";
    +
    +// get the underlying queryable's data
    +const data = sp.web.__data;
    +
    +console.log(JSON.stringify(data, null, 2));
    +
    +

    JSON

    +

    You can also get a representation of the wrapped instance in JSON format consisting of all its own properties and values.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/queryable/debug";
    +
    +// get the underlying queryable's as JSON
    +const data = sp.web.__json();
    +
    +console.log(JSON.stringify(data, null, 2));
    +
    +

    Deep Trace

    +

    Deep tracing is the ability to write every property and method access to the log. This produces VERY verbose output but can be helpful in situations where you need to trace how things are called and when within the Proxy. You enable deep tracing using the __enableDeepTrace method and disable using __disableDeepTrace.

    +
    import { Logger, ConsoleListener } from "@pnp/logging";
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/queryable/debug";
    +
    +Logger.subscribe(new ConsoleListener());
    +
    +// grab an instance to enable deep trace
    +const web = sp.web;
    +
    +// enable deep trace on the instance
    +web.__enableDeepTrace();
    +
    +const y = await web.lists();
    +
    +// disable deep trace
    +web.__disableDeepTrace();
    +
    +

    The example above produces the following output:

    +
    Message: get ::> lists
    +Message: get ::> lists
    +Message: get ::> toUrl
    +Message: get ::> toUrl
    +Message: get ::> data
    +Message: get ::> data
    +Message: get ::> _data
    +Message: get ::> query
    +Message: get ::> query
    +Message: get ::> data
    +Message: get ::> data
    +Message: get ::> _data
    +Message: get ::> _data
    +Message: get ::> data
    +Message: get ::> data
    +Message: get ::> _data
    +Message: get ::> _data
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {}
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {}
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request.
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request.
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {}
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {}
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {}
    +Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {}
    +Message: get ::> __disableDeepTrace
    +Message: get ::> __disableDeepTrace
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/extensions/index.html b/v2/odata/extensions/index.html new file mode 100644 index 000000000..c250324dc --- /dev/null +++ b/v2/odata/extensions/index.html @@ -0,0 +1,2581 @@ + + + + + + + + + + + + + + + + + + Extending an OData library - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    Extensions

    +

    introduced in 2.0.0

    +

    Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invocable. You can control any behavior of the library with extensions.

    +
    +

    Extensions do not work in ie11 compatibility mode. This is by design.

    +
    +

    Types of Extensions

    +

    There are three types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.

    +

    Function Extensions

    +

    The first type is a simple function with a signature:

    +
    (op: "apply" | "get" | "has" | "set", target: T, ...rest: any[]): void
    +
    +

    This function is passed the current operation as the first argument, currently one of "apply", "get", "has", or "set". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.

    +

    Named Extensions

    +

    Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables.

    +
    import { extendFactory } from "@pnp/queryable";
    +import { sp, List, Lists, IWeb, ILists, List, IList, Web } from "@pnp/sp/presets/all";
    +import { escapeQueryStrValue } from "@pnp/sp/utils/escapeQueryStrValue";
    +
    +// create a plain object with the props and methods we want to add/change
    +const myExtensions = {
    +    // override the lists property
    +    get lists(this: IWeb): ILists {
    +        // we will always order our lists by title and select just the Title for ALL calls (just as an example)
    +        return Lists(this).orderBy("Title").select("Title");
    +    },
    +    // override the getByTitle method
    +    getByTitle: function (this: ILists, title: string): IList {
    +        // in our example our list has moved, so we rewrite the request on the fly
    +        if (title === "List1") {
    +            return List(this, `getByTitle('List2')`);
    +        } else {
    +            // you can't at this point call the "base" method as you will end up in loop within the proxy
    +            // so you need to ensure you patch/include any original functionality you need
    +            return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
    +        }
    +    },
    +};
    +
    +// register all the named Extensions
    +extendFactory(Web, myExtensions);
    +
    +// this will use our extension to ensure the lists are ordered
    +const lists = await sp.web.lists();
    +
    +console.log(JSON.stringify(lists, null, 2));
    +
    +// we will get the items from List1 but within the extension it is rewritten as List2
    +const items = await sp.web.lists.getByTitle("List1").items();
    +
    +console.log(JSON.stringify(items.length, null, 2));
    +
    +

    ProxyHandler Extensions

    +

    You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work.

    +
    import { extendFactory } from "@pnp/queryable";
    +import { sp, Lists, IWeb, ILists, Web } from "@pnp/sp/presets/all";
    +import { escapeQueryStrValue } from "@pnp/sp/utils/escapeSingleQuote";
    +
    +const myExtensions = {
    +    get: (target, p: string | number | symbol, _receiver: any) => {
    +        switch (p) {
    +            case "getByTitle":
    +                return (title: string) => {
    +
    +                    // in our example our list has moved, so we rewrite the request on the fly
    +                    if (title === "LookupList") {
    +                        return List(target, `getByTitle('OrderByList')`);
    +                    } else {
    +                        // you can't at this point call the "base" method as you will end up in loop within the proxy
    +                        // so you need to ensure you patch/include any original functionality you need
    +                        return List(target, `getByTitle('${escapeQueryStrValue(title)}')`);
    +                    }
    +                };
    +        }
    +    },
    +};
    +
    +extendFactory(Web, myExtensions);
    +
    +const lists = sp.web.lists;
    +const items = await lists.getByTitle("LookupList").items();
    +
    +console.log(JSON.stringify(items.length, null, 2));
    +
    +

    Registering Extensions

    +

    You can register Extensions either globally, on an invocable factory, or on a per-object basis, and you can register a single extension or an array of Extensions.

    +

    Global Registration

    +

    Globally registering an extension allows you to inject functionality into every invocable that is instantiated within your application. It is important to remember that processing extensions happens on ALL property access and method invocation operations - so global extensions should be used sparingly.

    +
    import { extendGlobal } from "@pnp/queryable";
    +
    +// we can add a logging method to very verbosely track what things are called in our application
    +extendGlobal((op: string, _target: any, ...rest: any[]): void => {
    +        switch (op) {
    +            case "apply":
    +                Logger.write(`${op} ::> ()`, LogLevel.Info);
    +                break;
    +                case "has":
    +            case "get":
    +            case "set":
    +                Logger.write(`${op} ::> ${rest[0]}`, LogLevel.Info);
    +                break;
    +            default:
    +                Logger.write(`unknown ${op}`, LogLevel.Info);
    +        }
    +});
    +
    +

    Factory Registration

    +

    The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList.

    +
    import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import { IWeb, Web } from "@pnp/sp/webs";
    +import { ILists, Lists } from "@pnp/sp/lists";
    +import { extendFactory } from "@pnp/queryable";
    +import { sp } from "@pnp/sp";
    +
    +// sets up the types correctly when importing across your application
    +declare module "@pnp/sp/webs/types" {
    +
    +    // we need to extend the interface
    +    interface IWeb {
    +        orderedLists: ILists;
    +    }
    +}
    +
    +// sets up the types correctly when importing across your application
    +declare module "@pnp/sp/lists/types" {
    +
    +    // we need to extend the interface
    +    interface ILists {
    +        getOrderedListsQuery: (this: ILists) => ILists;
    +    }
    +}
    +
    +extendFactory(Web, {
    +    // add an ordered lists property
    +    get orderedLists(this: IWeb): ILists {
    +        return this.lists.getOrderedListsQuery();
    +    },
    +});
    +
    +extendFactory(Lists, {
    +    // add an ordered lists property
    +    getOrderedListsQuery(this: ILists): ILists {
    +        return this.top(10).orderBy("Title").select("Title");
    +    },
    +});
    +
    +// regardless of how we access the web and lists collections our extensions remain with all new instance based on
    +const web = Web("https://tenant.sharepoint.com/sites/dev/");
    +const lists1 = await web.orderedLists();
    +console.log(JSON.stringify(lists1, null, 2));
    +
    +const lists2 = await Web("https://tenant.sharepoint.com/sites/dev/").orderedLists();
    +console.log(JSON.stringify(lists2, null, 2));
    +
    +const lists3 = await sp.web.orderedLists();
    +console.log(JSON.stringify(lists3, null, 2));
    +
    +

    Instance Registration

    +

    You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances.

    +
    +

    Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are.

    +
    +

    Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance.

    +
    import { extendObj } from "@pnp/queryable";
    +import { sp, List, ILists } from "@pnp/sp/presets/all";
    +
    +const myExtensions = {
    +    getByTitle: function (this: ILists, title: string) {
    +        // in our example our list has moved, so we rewrite the request on the fly
    +        if (title === "List1") {
    +            return List(this, "getByTitle('List2')");
    +        } else {
    +            // you can't at this point call the "base" method as you will end up in loop within the proxy
    +            // so you need to ensure you patch/include any original functionality you need
    +            return List(this, `getByTitle('${escapeQueryStrValue(title)}')`);
    +        }
    +    },
    +};
    +
    +const lists =  extendObj(sp.web.lists, myExtensions);
    +const items = await lists.getByTitle("LookupList").items();
    +
    +console.log(JSON.stringify(items.length, null, 2));
    +
    +

    Enable & Disable Extensions and Clear Global Extensions

    +

    Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed.

    +
    import { enableExtensions, disableExtensions, clearGlobalExtensions } from "@pnp/queryable";
    +
    +// disable Extensions
    +disableExtensions();
    +
    +// enable Extensions
    +enableExtensions();
    +
    +// clear all the globally registered extensions
    +clearGlobalExtensions();
    +
    +

    Order of Operations

    +

    It is important to understand the order in which extensions are executed and when a value is returned. Instance extensions* are always called first, followed by global Extensions - in both cases they are called in the order they were registered. This allows you to perhaps have some global functionality while maintaining the ability to override it again at the instance level. IF an extension returns a value other than undefined that value is returned and no other extensions are processed.

    +
    +

    *extensions applied via an extended factory are considered instance extensions

    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/index.html b/v2/odata/index.html new file mode 100644 index 000000000..705e73326 --- /dev/null +++ b/v2/odata/index.html @@ -0,0 +1,2259 @@ + + + + + + + + + + + + + + + + + + odata - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/queryable

    +

    npm version

    +

    This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata +library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure +the core code is solid and well tested, with any updates benefitting all inheriting libraries.

    +

    Getting Started

    +

    Install the library and required dependencies

    +

    npm install @pnp/logging @pnp/core @pnp/queryable --save

    +

    Library Topics

    + + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/odata-batch/index.html b/v2/odata/odata-batch/index.html new file mode 100644 index 000000000..334e7ac4d --- /dev/null +++ b/v2/odata/odata-batch/index.html @@ -0,0 +1,2250 @@ + + + + + + + + + + + + + + + + + + OData Batching - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/queryable/odatabatch

    +

    This module contains an abstract class used as a base when inheriting libraries support batching.

    +

    ODataBatchRequestInfo

    +

    This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will +be responsible for processing that info by implementing the abstract executeImpl method.

    +

    ODataBatch

    +

    Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp +and @pnp/graph modules.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/parsers/index.html b/v2/odata/parsers/index.html new file mode 100644 index 000000000..76fc533e6 --- /dev/null +++ b/v2/odata/parsers/index.html @@ -0,0 +1,2355 @@ + + + + + + + + + + + + + + + + + + Parsers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/queryable/parsers

    +

    This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need.

    +

    ODataDefaultParser

    +

    The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request.

    +
    import { sp } from "@pnp/sp";
    +import { JSONParser } from "@pnp/queryable";
    +
    +try {
    +
    +    const parser = new JSONParser();
    +
    +    // this always throws a 404 error
    +    await sp.web.getList("doesn't exist").get(parser);
    +
    +} catch (e) {
    +
    +    // we can check for the property "isHttpRequestError" to see if this is an instance of our class
    +    // this gets by all the many limitations of subclassing Error and type detection in JavaScript
    +    if (e.hasOwnProperty("isHttpRequestError")) {
    +
    +        console.log("e is HttpRequestError");
    +
    +        // now we can access the various properties and make use of the response object.
    +        // at this point the body is unread
    +        console.log(`status: ${e.status}`);
    +        console.log(`statusText: ${e.statusText}`);
    +
    +        const json = await e.response.clone().json();
    +        console.log(JSON.stringify(json));
    +        const text = await e.response.clone().text();
    +        console.log(text);
    +        const headers = e.response.headers;
    +    }
    +
    +    console.error(e);
    +}
    +
    +

    TextParser

    +

    Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files.

    +

    BlobParser

    +

    Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files.

    +

    JSONParser

    +

    Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files.

    +

    BufferParser

    +

    Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files.

    +

    LambdaParser

    +

    Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type.

    +
    import { LambdaParser } from "@pnp/queryable";
    +import { sp } from "@pnp/sp";
    +
    +// here a simple parser duplicating the functionality of the JSONParser
    +const parser = new LambdaParser((r: Response) => r.json());
    +
    +const webDataJson = await sp.web.get(parser);
    +
    +console.log(webDataJson);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/pipeline/index.html b/v2/odata/pipeline/index.html new file mode 100644 index 000000000..2f8094dde --- /dev/null +++ b/v2/odata/pipeline/index.html @@ -0,0 +1,2297 @@ + + + + + + + + + + + + + + + + + + Pipeline - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/queryable/pipeline

    +

    All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext<T> interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline.

    +

    interface RequestContext<T>

    +

    The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required.

    +
    interface RequestContext<T> {
    +    batch: ODataBatch;
    +    batchDependency: () => void;
    +    cachingOptions: ICachingOptions;
    +    hasResult?: boolean;
    +    isBatched: boolean;
    +    isCached: boolean;
    +    options: FetchOptions;
    +    parser: ODataParser<T>;
    +    pipeline: Array<(c: RequestContext<T>) => Promise<RequestContext<T>>>;
    +    requestAbsoluteUrl: string;
    +    requestId: string;
    +    result?: T;
    +    verb: string;
    +    clientFactory: () => RequestClient;
    +}
    +
    +

    requestPipelineMethod decorator

    +

    The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existence of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed.

    +
    @requestPipelineMethod(true)
    +public static myPipelineMethod<T>(context: RequestContext<T>): Promise<RequestContext<T>> {
    +
    +    return new Promise<RequestContext<T>>(resolve => {
    +
    +        // do something
    +
    +        resolve(context);
    +    });
    +}
    +
    +

    Default Pipeline

    +
      +
    1. logs the start of the request
    2. +
    3. checks the cache for a value based on the context's cache settings
    4. +
    5. sends the request if no value from found in the cache
    6. +
    7. logs the end of the request
    8. +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/odata/queryable/index.html b/v2/odata/queryable/index.html new file mode 100644 index 000000000..6778cab75 --- /dev/null +++ b/v2/odata/queryable/index.html @@ -0,0 +1,2455 @@ + + + + + + + + + + + + + + + + + + Queryable - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/queryable/queryable

    +

    The Queryable class is the base class for all of the libraries building fluent request apis.

    +

    abstract class ODataQueryable<BatchType extends ODataBatch>

    +

    This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls.

    +

    Properties

    +

    query

    +

    Provides access to the query string builder for this url

    +

    Public Methods

    +

    concat

    +

    Directly concatenates the supplied string to the current url, not normalizing "/" chars

    +

    configure

    +

    Sets custom options for current object and all derived objects accessible via chaining

    +
    import { ConfigOptions } from "@pnp/queryable";
    +import { sp } from "@pnp/sp";
    +
    +const headers: ConfigOptions = {
    +    Accept: 'application/json;odata=nometadata'
    +};
    +
    +// here we use configure to set the headers value for all child requests of the list instance
    +const list = sp.web.lists.getByTitle("List1").configure({ headers });
    +
    +// this will use the values set in configure
    +list.items().then(items => console.log(JSON.stringify(items, null, 2));
    +
    +

    For reference the ConfigOptions interface is shown below:

    +
    export interface ConfigOptions {
    +    headers?: string[][] | { [key: string]: string } | Headers;
    +    mode?: "navigate" | "same-origin" | "no-cors" | "cors";
    +    credentials?: "omit" | "same-origin" | "include";
    +    cache?: "default" | "no-store" | "reload" | "no-cache" | "force-cache" | "only-if-cached";
    +}
    +
    +

    configureFrom

    +

    Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance.

    +

    usingCaching

    +

    Enables caching for this request. See caching for more details.

    +
    import { sp } from "@pnp/sp"
    +
    +sp.web.usingCaching()().then(...);
    +
    +

    inBatch

    +

    Adds this query to the supplied batch

    +

    toUrl

    +

    Gets the current url

    +

    abstract toUrlAndQuery()

    +

    When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request

    +

    get

    +

    Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/packages/index.html b/v2/packages/index.html new file mode 100644 index 000000000..f22b360a2 --- /dev/null +++ b/v2/packages/index.html @@ -0,0 +1,2251 @@ + + + + + + + + + + + + + + + + + + Packages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Packages

    +

    The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope.

    +

    The latest published version is npm version.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    @pnp/
    adaljsclientProvides an adaljs wrapper suitable for use with PnPjs
    commonProvides shared functionality across all pnp libraries
    config-storeProvides a way to manage configuration within your application
    graphProvides a fluent api for working with Microsoft Graph
    loggingLight-weight, subscribable logging framework
    msaljsclientProvides an msal wrapper suitable for use with PnPjs
    nodejsProvides functionality enabling the @pnp libraries within nodejs
    odataProvides shared odata functionality and base classes
    spProvides a fluent api for working with SharePoint REST
    sp-addinhelpersProvides functionality for working within SharePoint add-ins
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/pnpjs/index.html b/v2/pnpjs/index.html new file mode 100644 index 000000000..f0c0222c4 --- /dev/null +++ b/v2/pnpjs/index.html @@ -0,0 +1,2281 @@ + + + + + + + + + + + + + + + + + + pnpjs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    PnPjs

    +

    This package is a rollup package of all the other libraries for scenarios where you would prefer to access all of the code from a single file. Examples would be importing a single file into a script editor webpart or using the library in other ways that benefit from a single file. You will not be able to take advantage of selective imports using this bundle.

    +
    +

    Our recommendation is to import the packages directly into your project, or to create a custom bundle. This package is mostly provided to help folks with backward-compatibility needs.

    +
    +

    Script Editor Webpart

    +

    The below is an example of using the pnp.js bundle within a Script Editor webpart. This script editor example is provided for folks on older version of SharePoint - when possible your first choice is SharePoint Framework.

    +

    You will need to grab the pnp.js bundle file from the dist folder of the pnpjs package and upload it to a location where you can reference it from without your script editor webparts.

    +
    +

    *This is included as a reference for backward compatibility. The script editor webpart is no longer available in SharePoint online. In addition, see our General Statement on Polyfills and IE11

    +
    +
    <script src="https://mytenant.sharepoint.com/sites/dev/Shared%20Documents/pnp2bundle/pnp.js"></script>
    +<!-- Optional to include the IE11 polyfill package -->
    +<script src="https://unpkg.com/@pnp/polyfill-ie11"></script>
    +<script>
    +
    +document.onreadystatechange = async function() {
    +
    +    if(document.readyState === "complete") {
    +
    +        // because this is a UMD bundle there is a global root object named "pnp"
    +        const a = await pnp.sp.web.lists();
    +        document.getElementById("pnpexample").innerHTML = JSON.stringify(a);
    +    }
    +}
    +</script>
    +<div id="pnpexample"></div>
    +
    +

    Access Library Features

    +

    Within the bundle all of the classes and methods are exported at the root object, with the exports from sp and graph libraries contained with NS variables to avoid naming conflicts. So if you need to access say the "Web" factory you can do so:

    +
    const web = pnp.SPNS.Web("https://something.sharepoint.com");
    +const lists = await web.lists();
    +
    +
    pnp.GraphNS.*
    +
    +

    Individual libraries can also be accessed for their exports:

    +
    pnp.Logger.subscribe(new pnp.ConsoleListener());
    +pnp.log.write("hello");
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/search/search_index.json b/v2/search/search_index.json new file mode 100644 index 000000000..fb2111733 --- /dev/null +++ b/v2/search/search_index.json @@ -0,0 +1 @@ +{"config":{"lang":["en"],"min_search_length":3,"prebuild_index":false,"separator":"[\\s\\-]+"},"docs":[{"location":"","text":"PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community. Animation of the library in use, note intellisense help in building your queries General Guidance \u00b6 These articles provide general guidance for working with the libraries. If you are migrating from v1 please review the transition guide . Getting Started Authentication Get Started Contributing npm scripts Polyfills Packages \u00b6 Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins Authentication \u00b6 We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out! Issues, Questions, Ideas \u00b6 Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project. Changelog \u00b6 Please review the CHANGELOG for release details on all library changes. Code of Conduct \u00b6 This project has adopted the Microsoft Open Source Code of Conduct . For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments. \"Sharing is Caring\" \u00b6 Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program . Disclaimer \u00b6 THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.","title":"Home"},{"location":"#general-guidance","text":"These articles provide general guidance for working with the libraries. If you are migrating from v1 please review the transition guide . Getting Started Authentication Get Started Contributing npm scripts Polyfills","title":"General Guidance"},{"location":"#packages","text":"Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins","title":"Packages"},{"location":"#authentication","text":"We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!","title":"Authentication"},{"location":"#issues-questions-ideas","text":"Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.","title":"Issues, Questions, Ideas"},{"location":"#changelog","text":"Please review the CHANGELOG for release details on all library changes.","title":"Changelog"},{"location":"#code-of-conduct","text":"This project has adopted the Microsoft Open Source Code of Conduct . For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.","title":"Code of Conduct"},{"location":"#sharing-is-caring","text":"Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program .","title":"\"Sharing is Caring\""},{"location":"#disclaimer","text":"THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.","title":"Disclaimer"},{"location":"SPFx-on-premises/","text":"Workaround for on-premises SPFx TypeScript Version (SharePoint 2016 or 2019) \u00b6 Note this article applies to version 1.4.1 SharePoint Framework projects targeting on-premises only. When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premises it installs TypeScript version 2.2.2 (SP2016) or 2.4.2/2.4.1 (SP2019). Unfortunately this library relies on 3.6.4 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. npm i npm i -g rimraf # used to remove the node_modules folder (much better/faster) Ensure that the @pnp/sp package is already installed npm i @pnp/sp Remove the package-lock.json file & node_modules rimraf node_modules folder and execute npm install Open package-lock.json from the root folder Search for \"typescript\" or similar with version 2.4.1 (SP2019) 2.2.2 (SP2016) Replace \"2.4.1\" or \"2.2.2\" with \"3.6.4\" Search for the next \"typescript\" occurrence and replace the block with: JSON \"typescript\": { \"version\": \"3.6.4\", \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz\", \"integrity\": \"sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==\", \"dev\": true } Remove node_modules folder rimraf node_modules Run npm install Alternative using npm-force-resolutions \u00b6 Install resolutions package and TypeScript providing considered version explicitly: bash npm i -D npm-force-resolutions typescript@3.6.4 Add a resolution for TypeScript and preinstall script into package.json to a corresponding code blocks: JSON { \"scripts\": { \"preinstall\": \"npx npm-force-resolutions\" }, \"resolutions\": { \"typescript\": \"3.6.4\" } } Run npm install to trigger preinstall script and bumping TypeScript version into package-lock.json Run npm run build , should produce no errors Installing additional dependencies should be safe then.","title":"SPFx On-Premises"},{"location":"SPFx-on-premises/#workaround-for-on-premises-spfx-typescript-version-sharepoint-2016-or-2019","text":"Note this article applies to version 1.4.1 SharePoint Framework projects targeting on-premises only. When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premises it installs TypeScript version 2.2.2 (SP2016) or 2.4.2/2.4.1 (SP2019). Unfortunately this library relies on 3.6.4 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. npm i npm i -g rimraf # used to remove the node_modules folder (much better/faster) Ensure that the @pnp/sp package is already installed npm i @pnp/sp Remove the package-lock.json file & node_modules rimraf node_modules folder and execute npm install Open package-lock.json from the root folder Search for \"typescript\" or similar with version 2.4.1 (SP2019) 2.2.2 (SP2016) Replace \"2.4.1\" or \"2.2.2\" with \"3.6.4\" Search for the next \"typescript\" occurrence and replace the block with: JSON \"typescript\": { \"version\": \"3.6.4\", \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz\", \"integrity\": \"sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==\", \"dev\": true } Remove node_modules folder rimraf node_modules Run npm install","title":"Workaround for on-premises SPFx TypeScript Version (SharePoint 2016 or 2019)"},{"location":"SPFx-on-premises/#alternative-using-npm-force-resolutions","text":"Install resolutions package and TypeScript providing considered version explicitly: bash npm i -D npm-force-resolutions typescript@3.6.4 Add a resolution for TypeScript and preinstall script into package.json to a corresponding code blocks: JSON { \"scripts\": { \"preinstall\": \"npx npm-force-resolutions\" }, \"resolutions\": { \"typescript\": \"3.6.4\" } } Run npm install to trigger preinstall script and bumping TypeScript version into package-lock.json Run npm run build , should produce no errors Installing additional dependencies should be safe then.","title":"Alternative using npm-force-resolutions"},{"location":"getting-started/","text":"Getting Started \u00b6 These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. If you need to support older browsers please review the article on polyfills for required functionality. Install \u00b6 First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project. npm install @pnp/sp @pnp/graph --save Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples. import { getRandomString } from \"@pnp/core\"; (function() { // get and log a random string console.log(getRandomString(20)); })() Getting Started with SharePoint Framework \u00b6 The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises please read this note on a workaround for the included TypeScript version. If you are targeting SharePoint online you do not need to take any additional steps. Establish Context \u00b6 Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the SPFx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. The setup is always done in the onInit method to ensure it runs before your other life-cycle code. You can also set any other settings at this time. Using @pnp/core setup \u00b6 import { setup as pnpSetup } from \"@pnp/core\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present pnpSetup({ spfxContext: this.context }); }); } // ... Using @pnp/sp setup \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... Sp setup also supports passing just the SPFx context object directly as this is the most common case import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ... Using @pnp/graph setup \u00b6 import { graph } from \"@pnp/graph/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ... Establish context within an SPFx service \u00b6 Because you do not have full access to the context object within a service you need to setup things a little differently. If you do not need AAD tokens you can leave that part out and specify just the pageContext. import { ServiceKey, ServiceScope } from \"@microsoft/sp-core-library\"; import { PageContext } from \"@microsoft/sp-page-context\"; import { AadTokenProviderFactory } from \"@microsoft/sp-http\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; export interface ISampleService { getLists(): Promise; } export class SampleService { public static readonly serviceKey: ServiceKey = ServiceKey.create('SPFx:SampleService', SampleService); constructor(serviceScope: ServiceScope) { serviceScope.whenFinished(() => { const pageContext = serviceScope.consume(PageContext.serviceKey); const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey); // we need to \"spoof\" the context object with the parts we need for PnPjs sp.setup({ spfxContext: { aadTokenProviderFactory: tokenProviderFactory, pageContext: pageContext, } }); // This approach also works if you do not require AAD tokens // you don't need to do both // sp.setup({ // sp : { // baseUrl : pageContext.web.absoluteUrl // } // }); }); } public getLists(): Promise { return sp.web.lists(); } } Connect to SharePoint from Node \u00b6 Please see the main article on how we support node versions that require commonjs modules. npm i @pnp/sp-commonjs @pnp/nodejs-commonjs This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: import { sp } from \"@pnp/sp-commonjs\"; import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. // configure your node options (only once in your application) sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // make a call to SharePoint and log it in the console sp.web.select(\"Title\", \"Description\")().then(w => { console.log(JSON.stringify(w, null, 4)); }); Connect to Microsoft Graph From Node \u00b6 Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example. npm i @pnp/graph-commonjs @pnp/nodejs-commonjs Now we need to import what we'll need to call graph import { graph } from \"@pnp/graph-commonjs\"; import { AdalFetchClient } from \"@pnp/nodejs-commonjs\"; Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{mytenant}.onmicrosoft.com\", \"{application id}\", \"{application secret}\"); }, }, }); // make a call to Graph and get all the groups graph.groups().then(g => { console.log(JSON.stringify(g, null, 4)); }); Getting Started outside SharePoint Framework \u00b6 In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options. Set baseUrl through setup \u00b6 Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when working against unpatched versions of SharePoint 2013 as discussed here . This is optional for 2016 or SharePoint Online. The library does not support setting the headers to use nometadata as we rely on the metadata in the response to do some of the more complicated functions. Some of the pure data calls will probably work but it is not a supported configuration. import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { headers: { Accept: \"application/json;odata=verbose\", }, baseUrl: \"{Absolute SharePoint Web URL}\" }, }); const w = await sp.web(); Create Web instances directly \u00b6 Using this method you create the web directly with the url you want to use as the base. import { Web } from \"@pnp/sp/presets/all\"; const web = Web(\"{Absolute SharePoint Web URL}\"); const w = await web(); Next Steps \u00b6 Be sure to review the article describing all of the available settings across the libraries.","title":"Getting Started"},{"location":"getting-started/#getting-started","text":"These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. If you need to support older browsers please review the article on polyfills for required functionality.","title":"Getting Started"},{"location":"getting-started/#install","text":"First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project. npm install @pnp/sp @pnp/graph --save Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples. import { getRandomString } from \"@pnp/core\"; (function() { // get and log a random string console.log(getRandomString(20)); })()","title":"Install"},{"location":"getting-started/#getting-started-with-sharepoint-framework","text":"The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises please read this note on a workaround for the included TypeScript version. If you are targeting SharePoint online you do not need to take any additional steps.","title":"Getting Started with SharePoint Framework"},{"location":"getting-started/#establish-context","text":"Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the SPFx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. The setup is always done in the onInit method to ensure it runs before your other life-cycle code. You can also set any other settings at this time.","title":"Establish Context"},{"location":"getting-started/#using-pnpcore-setup","text":"import { setup as pnpSetup } from \"@pnp/core\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present pnpSetup({ spfxContext: this.context }); }); } // ...","title":"Using @pnp/core setup"},{"location":"getting-started/#using-pnpsp-setup","text":"import { sp } from \"@pnp/sp/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... Sp setup also supports passing just the SPFx context object directly as this is the most common case import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ...","title":"Using @pnp/sp setup"},{"location":"getting-started/#using-pnpgraph-setup","text":"import { graph } from \"@pnp/graph/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ...","title":"Using @pnp/graph setup"},{"location":"getting-started/#establish-context-within-an-spfx-service","text":"Because you do not have full access to the context object within a service you need to setup things a little differently. If you do not need AAD tokens you can leave that part out and specify just the pageContext. import { ServiceKey, ServiceScope } from \"@microsoft/sp-core-library\"; import { PageContext } from \"@microsoft/sp-page-context\"; import { AadTokenProviderFactory } from \"@microsoft/sp-http\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; export interface ISampleService { getLists(): Promise; } export class SampleService { public static readonly serviceKey: ServiceKey = ServiceKey.create('SPFx:SampleService', SampleService); constructor(serviceScope: ServiceScope) { serviceScope.whenFinished(() => { const pageContext = serviceScope.consume(PageContext.serviceKey); const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey); // we need to \"spoof\" the context object with the parts we need for PnPjs sp.setup({ spfxContext: { aadTokenProviderFactory: tokenProviderFactory, pageContext: pageContext, } }); // This approach also works if you do not require AAD tokens // you don't need to do both // sp.setup({ // sp : { // baseUrl : pageContext.web.absoluteUrl // } // }); }); } public getLists(): Promise { return sp.web.lists(); } }","title":"Establish context within an SPFx service"},{"location":"getting-started/#connect-to-sharepoint-from-node","text":"Please see the main article on how we support node versions that require commonjs modules. npm i @pnp/sp-commonjs @pnp/nodejs-commonjs This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: import { sp } from \"@pnp/sp-commonjs\"; import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. // configure your node options (only once in your application) sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // make a call to SharePoint and log it in the console sp.web.select(\"Title\", \"Description\")().then(w => { console.log(JSON.stringify(w, null, 4)); });","title":"Connect to SharePoint from Node"},{"location":"getting-started/#connect-to-microsoft-graph-from-node","text":"Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example. npm i @pnp/graph-commonjs @pnp/nodejs-commonjs Now we need to import what we'll need to call graph import { graph } from \"@pnp/graph-commonjs\"; import { AdalFetchClient } from \"@pnp/nodejs-commonjs\"; Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{mytenant}.onmicrosoft.com\", \"{application id}\", \"{application secret}\"); }, }, }); // make a call to Graph and get all the groups graph.groups().then(g => { console.log(JSON.stringify(g, null, 4)); });","title":"Connect to Microsoft Graph From Node"},{"location":"getting-started/#getting-started-outside-sharepoint-framework","text":"In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options.","title":"Getting Started outside SharePoint Framework"},{"location":"getting-started/#set-baseurl-through-setup","text":"Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when working against unpatched versions of SharePoint 2013 as discussed here . This is optional for 2016 or SharePoint Online. The library does not support setting the headers to use nometadata as we rely on the metadata in the response to do some of the more complicated functions. Some of the pure data calls will probably work but it is not a supported configuration. import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { headers: { Accept: \"application/json;odata=verbose\", }, baseUrl: \"{Absolute SharePoint Web URL}\" }, }); const w = await sp.web();","title":"Set baseUrl through setup"},{"location":"getting-started/#create-web-instances-directly","text":"Using this method you create the web directly with the url you want to use as the base. import { Web } from \"@pnp/sp/presets/all\"; const web = Web(\"{Absolute SharePoint Web URL}\"); const w = await web();","title":"Create Web instances directly"},{"location":"getting-started/#next-steps","text":"Be sure to review the article describing all of the available settings across the libraries.","title":"Next Steps"},{"location":"nodejs-support/","text":"Working in Nodejs \u00b6 As outlined on the getting started page you can easily use the library with Nodejs, but there are some key differences you need to consider. But first a little history, you can skip this part if you just want to see how things work but we felt some folks might be interested. To make selective imports work we need to support es module syntax for client-side environments such as SPFx development. All versions of Nodejs that are currently LTS do not support es modules without flags (as of when this was written). We thought we had a scheme to handle this following the available guidance but ultimately it didn't work across all node versions and we unpublished 2.0.1. CommonJS Libraries \u00b6 Because of the difficulties of working with es modules in node we recommend using our mirror packages providing commonjs modules. These can be installed by using the package name and appending -commonjs, such as: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs These packages are built from the same source and released at the same time so all updates are included with each release. The only difference is that for the sp-commonjs and graph-commonjs packages we target the \"all\" preset as the entry point. This makes things a little easier in node where bundle sizes aren't an issue. You can see this in the nodejs-app sample . Here is that sample explained fully: Install Libraries \u00b6 We want to make a simple request to SharePoint so we need to first install the modules we need: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs --save We will also install TypeScript: npm install typescript --save-dev index.ts \u00b6 We will create an index.ts file and add the following code. You will need to update the site url, client id, and client secret to your values. This should be done using a settings file or something like Azure KeyVault for production, but for this example it is good enough. // our imports come from the -commonjs libs import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; // we call setup to use the node client sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{ site url }\", \"{ client id }\", \"{ client secret }\"); }, }, }); async function makeRequest() { // make a request to get the web's details const w = await sp.web(); console.log(JSON.stringify(w, null, 2)); } // get past no await at root of app makeRequest(); Don't forget you will need to register an app to get the client id and secret. Add a tsconfig.json \u00b6 Not strictly necessary but very useful to include a tsconfig.json to control how tsc transpiles your code to JavaScript { \"compilerOptions\": { \"module\": \"commonjs\", \"target\": \"esnext\", \"moduleResolution\": \"node\", \"declaration\": true, \"outDir\": \"dist\", \"skipLibCheck\": true, \"sourceMap\": true, \"lib\": [ \"dom\", \"esnext\" ] }, \"files\": [ \"./index.ts\" ] } Add an script to package.json \u00b6 We add the \"start\" script to the default package.json { \"name\": \"nodejs-app\", \"version\": \"1.0.0\", \"description\": \"Sample nodejs app using PnPjs\", \"main\": \"index.js\", \"scripts\": { \"start\": \"tsc -p . && node dist/index.js\", \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\" }, \"author\": \"\", \"license\": \"MIT\", \"dependencies\": { \"@pnp/nodejs-commonjs\": \"^2.0.2-5\", \"@pnp/sp-commonjs\": \"^2.0.2-5\" }, \"devDependencies\": { \"typescript\": \"^3.7.5\" } } Run It \u00b6 You can now run your program using: npm start","title":"Working in Nodejs"},{"location":"nodejs-support/#working-in-nodejs","text":"As outlined on the getting started page you can easily use the library with Nodejs, but there are some key differences you need to consider. But first a little history, you can skip this part if you just want to see how things work but we felt some folks might be interested. To make selective imports work we need to support es module syntax for client-side environments such as SPFx development. All versions of Nodejs that are currently LTS do not support es modules without flags (as of when this was written). We thought we had a scheme to handle this following the available guidance but ultimately it didn't work across all node versions and we unpublished 2.0.1.","title":"Working in Nodejs"},{"location":"nodejs-support/#commonjs-libraries","text":"Because of the difficulties of working with es modules in node we recommend using our mirror packages providing commonjs modules. These can be installed by using the package name and appending -commonjs, such as: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs These packages are built from the same source and released at the same time so all updates are included with each release. The only difference is that for the sp-commonjs and graph-commonjs packages we target the \"all\" preset as the entry point. This makes things a little easier in node where bundle sizes aren't an issue. You can see this in the nodejs-app sample . Here is that sample explained fully:","title":"CommonJS Libraries"},{"location":"nodejs-support/#install-libraries","text":"We want to make a simple request to SharePoint so we need to first install the modules we need: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs --save We will also install TypeScript: npm install typescript --save-dev","title":"Install Libraries"},{"location":"nodejs-support/#indexts","text":"We will create an index.ts file and add the following code. You will need to update the site url, client id, and client secret to your values. This should be done using a settings file or something like Azure KeyVault for production, but for this example it is good enough. // our imports come from the -commonjs libs import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; // we call setup to use the node client sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{ site url }\", \"{ client id }\", \"{ client secret }\"); }, }, }); async function makeRequest() { // make a request to get the web's details const w = await sp.web(); console.log(JSON.stringify(w, null, 2)); } // get past no await at root of app makeRequest(); Don't forget you will need to register an app to get the client id and secret.","title":"index.ts"},{"location":"nodejs-support/#add-a-tsconfigjson","text":"Not strictly necessary but very useful to include a tsconfig.json to control how tsc transpiles your code to JavaScript { \"compilerOptions\": { \"module\": \"commonjs\", \"target\": \"esnext\", \"moduleResolution\": \"node\", \"declaration\": true, \"outDir\": \"dist\", \"skipLibCheck\": true, \"sourceMap\": true, \"lib\": [ \"dom\", \"esnext\" ] }, \"files\": [ \"./index.ts\" ] }","title":"Add a tsconfig.json"},{"location":"nodejs-support/#add-an-script-to-packagejson","text":"We add the \"start\" script to the default package.json { \"name\": \"nodejs-app\", \"version\": \"1.0.0\", \"description\": \"Sample nodejs app using PnPjs\", \"main\": \"index.js\", \"scripts\": { \"start\": \"tsc -p . && node dist/index.js\", \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\" }, \"author\": \"\", \"license\": \"MIT\", \"dependencies\": { \"@pnp/nodejs-commonjs\": \"^2.0.2-5\", \"@pnp/sp-commonjs\": \"^2.0.2-5\" }, \"devDependencies\": { \"typescript\": \"^3.7.5\" } }","title":"Add an script to package.json"},{"location":"nodejs-support/#run-it","text":"You can now run your program using: npm start","title":"Run It"},{"location":"npm-scripts/","text":"Supported NPM Scripts \u00b6 As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies. This article outlines the current scripts we've implemented and how to use them, with available options and examples. Start \u00b6 Executes the serve command npm start Serve \u00b6 Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node. npm run serve Test \u00b6 Runs the tests and coverage for the library. Starting with 2.3.0 ONLY MSAL auth is supported for running the tests. More details on setting up MSAL for node. Options \u00b6 There are several options you can provide to the test command. All of these need to be separated using a \"--\" double hyphen so they are passed to the spawned sub-commands. Test a Single Package \u00b6 --package or -p This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory. # run only sp tests npm test -- -p sp # run only logging tests npm test -- -package logging Run a Single Test File \u00b6 --single or --s You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags. # run only sp web tests npm test -- -p sp -s web # run only graph groups tests npm test -- -package graph -single groups Specify a Site \u00b6 --site By default every time you run the tests a new sub-site is created below the site specified in your settings file . You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option. This option can be used with any or none of the other testing options. # run only sp web tests with a certain site npm test -- -p sp -s web --site https://some.site.com/sites/dev Cleanup \u00b6 --cleanup If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted. # clean up our testing site npm test -- --cleanup Logging \u00b6 --logging If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output. # enable logging during testing npm test -- --logging spVerbose \u00b6 Added in 2.0.13 --spverbose This flag will enable \"verbose\" OData mode for SharePoint tests. This flag is compatible with other flags. npm test -- --spverbose build \u00b6 Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed. npm run build package \u00b6 Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published. npm run package lint \u00b6 Runs the linter. npm run lint clean \u00b6 Removes any generated folders from the working directory. npm run clean","title":"Npm Scripts"},{"location":"npm-scripts/#supported-npm-scripts","text":"As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies. This article outlines the current scripts we've implemented and how to use them, with available options and examples.","title":"Supported NPM Scripts"},{"location":"npm-scripts/#start","text":"Executes the serve command npm start","title":"Start"},{"location":"npm-scripts/#serve","text":"Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node. npm run serve","title":"Serve"},{"location":"npm-scripts/#test","text":"Runs the tests and coverage for the library. Starting with 2.3.0 ONLY MSAL auth is supported for running the tests. More details on setting up MSAL for node.","title":"Test"},{"location":"npm-scripts/#options","text":"There are several options you can provide to the test command. All of these need to be separated using a \"--\" double hyphen so they are passed to the spawned sub-commands.","title":"Options"},{"location":"npm-scripts/#test-a-single-package","text":"--package or -p This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory. # run only sp tests npm test -- -p sp # run only logging tests npm test -- -package logging","title":"Test a Single Package"},{"location":"npm-scripts/#run-a-single-test-file","text":"--single or --s You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags. # run only sp web tests npm test -- -p sp -s web # run only graph groups tests npm test -- -package graph -single groups","title":"Run a Single Test File"},{"location":"npm-scripts/#specify-a-site","text":"--site By default every time you run the tests a new sub-site is created below the site specified in your settings file . You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option. This option can be used with any or none of the other testing options. # run only sp web tests with a certain site npm test -- -p sp -s web --site https://some.site.com/sites/dev","title":"Specify a Site"},{"location":"npm-scripts/#cleanup","text":"--cleanup If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted. # clean up our testing site npm test -- --cleanup","title":"Cleanup"},{"location":"npm-scripts/#logging","text":"--logging If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output. # enable logging during testing npm test -- --logging","title":"Logging"},{"location":"npm-scripts/#spverbose","text":"Added in 2.0.13 --spverbose This flag will enable \"verbose\" OData mode for SharePoint tests. This flag is compatible with other flags. npm test -- --spverbose","title":"spVerbose"},{"location":"npm-scripts/#build","text":"Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed. npm run build","title":"build"},{"location":"npm-scripts/#package","text":"Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published. npm run package","title":"package"},{"location":"npm-scripts/#lint","text":"Runs the linter. npm run lint","title":"lint"},{"location":"npm-scripts/#clean","text":"Removes any generated folders from the working directory. npm run clean","title":"clean"},{"location":"packages/","text":"Packages \u00b6 The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins","title":"Packages"},{"location":"packages/#packages","text":"The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins","title":"Packages"},{"location":"transition-guide/","text":"Transition Guide \u00b6 We have worked to make moving from @pnp library 1. to 2. as painless as possible, however there are some changes to how things work. The below guide we have provided an overview of what it takes to transition between the libraries. If we missed something, please let us know in the issues list so we can update the guide. Thanks! Installing @pnp libraries \u00b6 In version 1.* the libraries were setup as peer dependencies of each other requiring you to install each of them separately. We continue to believe this correctly describes the relationship, but recognize that basically nothing in the world accounts for peer dependencies. So we have updated the libraries to be dependencies. This makes it easier to install into your projects as you only need to install a single library: npm i --save @pnp/sp Selective Imports \u00b6 Another big change in v2 is the ability to selectively import the pieces you need from the libraries. This allows you to have smaller bundles and works well with tree-shaking. It does require you to have more import statements, which can potentially be a bit confusing at first. The selective imports apply to the sp and graph libraries. To help explain let's take the example of the Web object. In v1 Web includes a reference to pretty much everything else in the entire sp library. Meaning that if you use web (and you pretty much have to) you hold a ref to all the other pieces (like Fields, Lists, ContentTypes) even if you aren't using them. Because of that tree shaking can't do anything to reduce the bundle size because it \"thinks\" you are using them simply because they have been imported. To solve this in v2 the Web object no longer contains references to anything, it is a bare object with a few methods. If you look at the source you will see that, for example, there is no longer a \"lists\" property. These properties and methods are now added through selectively importing the functionality you need: Selectively Import Web lists functionality \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports the functionality for lists associated only with web import \"@pnp/sp/lists/web\"; const r = await sp.web.lists(); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports all the functionality for lists import \"@pnp/sp/lists\"; const r = await sp.web.lists(); Each of the docs pages shows the selective import paths for each sub-module (lists, items, etc.). Presets \u00b6 In addition to the ability to selectively import functionality you can import presets. This allows you to import an entire set of functionality in a single line. At launch the sp library will support two presets \"all\" and \"core\" with the graph library supporting \"all\". Using the \"all\" preset will match the functionality of v1. This can save you time in transitioning your projects so you can update to selective imports later. For new projects we recommend using the selective imports from day 1. To update your V1 projects to V2 you can replace all instances of \"@pnp/sp\" with \"@pnp/sp/presets/all\" and things should work as before (though some class names or other things may have changed, please review the change log and the rest of this guide). // V1 way of doing things: import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes, } from \"@pnp/sp\"; // V2 way with selective imports import { sp } from \"@pnp/sp\"; import { ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/clientside-pages\"; // V2 way with preset \"all\" import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/presets/all\"; Invokable Objects \u00b6 Another new feature is the addition of invokable objects. Previously where you used \"get()\" to invoke a request you can now leave it off. We have left the .get method in place so everyone's code wasn't broken immediately upon transitioning. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // old way (still works) const r1 = sp.web(); // invokable const r2 = sp.web(); The benefit is that objects can now support default actions that are not \"get\" but might be \"post\". And you save typing a few extra characters. This still work the same as with select or any of the other odata methods: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // invokable const r = sp.web.select(\"Title\", \"Url\")(); Factory Functions & Interfaces \u00b6 Another change in the library is in the structure of exports. We are no longer exporting the objects themselves, rather we are only exposing factory functions and interfaces. This allows us to decouple what developers use from our internal implementation. For folks using the fluent chain starting with sp you shouldn't need to update your code. If you are using any of the v1 classes directly you should just need to remove the \"new\" keyword and update the import path. The factory functions signature matches the constructor signature of the v1 objects. // v1 import { Web } from \"@pnp/sp\"; const web: Web = new Web(\"some absolute url\"); const r1 = web(); // v2 import { Web, IWeb } from \"@pnp/sp/webs\"; const web: IWeb = Web(\"some absolute url\"); const r2 = web(); Extension Methods \u00b6 Another new capability in v2 is the ability to extend objects and factories. This allows you to easily add methods or properties on a per-object basis. Please see the full article on extension methods describing this great new capability. CDN publishing \u00b6 Starting with v2 we will no longer create bundles for each of the packages. Historically these are not commonly used, don't work perfectly for everyone (there are a lot of ways to bundle things), and another piece we need to maintain. Instead we encourage folks to create their own bundles , optimized for their particular scenario. This will result in smaller overall bundle size and allow folks to bundle things to match their scenario. Please review the article on creating your custom bundles to see how to tailor bundles to your needs. The PnPjs bundle will remain, though it is designed only for backwards compatibility and we strongly recommend creating your own bundles, or directly importing the libraries into your projects using selective imports. Drop client-svc and sp-taxonomy libraries \u00b6 These libraries were created to allow folks to access and manage SharePoint taxonomy and manage metadata. Given that there is upcoming support for taxonomy via a supported REST API we will drop these two libraries. If working with taxonomy remains a core requirement of your application and we do not yet have support for the new apis, please remain on v1 for the time being. As of 2.0.6 we support reading the modern taxonomy API. Docs here","title":"Transition Guide"},{"location":"transition-guide/#transition-guide","text":"We have worked to make moving from @pnp library 1. to 2. as painless as possible, however there are some changes to how things work. The below guide we have provided an overview of what it takes to transition between the libraries. If we missed something, please let us know in the issues list so we can update the guide. Thanks!","title":"Transition Guide"},{"location":"transition-guide/#installing-pnp-libraries","text":"In version 1.* the libraries were setup as peer dependencies of each other requiring you to install each of them separately. We continue to believe this correctly describes the relationship, but recognize that basically nothing in the world accounts for peer dependencies. So we have updated the libraries to be dependencies. This makes it easier to install into your projects as you only need to install a single library: npm i --save @pnp/sp","title":"Installing @pnp libraries"},{"location":"transition-guide/#selective-imports","text":"Another big change in v2 is the ability to selectively import the pieces you need from the libraries. This allows you to have smaller bundles and works well with tree-shaking. It does require you to have more import statements, which can potentially be a bit confusing at first. The selective imports apply to the sp and graph libraries. To help explain let's take the example of the Web object. In v1 Web includes a reference to pretty much everything else in the entire sp library. Meaning that if you use web (and you pretty much have to) you hold a ref to all the other pieces (like Fields, Lists, ContentTypes) even if you aren't using them. Because of that tree shaking can't do anything to reduce the bundle size because it \"thinks\" you are using them simply because they have been imported. To solve this in v2 the Web object no longer contains references to anything, it is a bare object with a few methods. If you look at the source you will see that, for example, there is no longer a \"lists\" property. These properties and methods are now added through selectively importing the functionality you need:","title":"Selective Imports"},{"location":"transition-guide/#selectively-import-web-lists-functionality","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports the functionality for lists associated only with web import \"@pnp/sp/lists/web\"; const r = await sp.web.lists(); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports all the functionality for lists import \"@pnp/sp/lists\"; const r = await sp.web.lists(); Each of the docs pages shows the selective import paths for each sub-module (lists, items, etc.).","title":"Selectively Import Web lists functionality"},{"location":"transition-guide/#presets","text":"In addition to the ability to selectively import functionality you can import presets. This allows you to import an entire set of functionality in a single line. At launch the sp library will support two presets \"all\" and \"core\" with the graph library supporting \"all\". Using the \"all\" preset will match the functionality of v1. This can save you time in transitioning your projects so you can update to selective imports later. For new projects we recommend using the selective imports from day 1. To update your V1 projects to V2 you can replace all instances of \"@pnp/sp\" with \"@pnp/sp/presets/all\" and things should work as before (though some class names or other things may have changed, please review the change log and the rest of this guide). // V1 way of doing things: import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes, } from \"@pnp/sp\"; // V2 way with selective imports import { sp } from \"@pnp/sp\"; import { ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/clientside-pages\"; // V2 way with preset \"all\" import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/presets/all\";","title":"Presets"},{"location":"transition-guide/#invokable-objects","text":"Another new feature is the addition of invokable objects. Previously where you used \"get()\" to invoke a request you can now leave it off. We have left the .get method in place so everyone's code wasn't broken immediately upon transitioning. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // old way (still works) const r1 = sp.web(); // invokable const r2 = sp.web(); The benefit is that objects can now support default actions that are not \"get\" but might be \"post\". And you save typing a few extra characters. This still work the same as with select or any of the other odata methods: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // invokable const r = sp.web.select(\"Title\", \"Url\")();","title":"Invokable Objects"},{"location":"transition-guide/#factory-functions-interfaces","text":"Another change in the library is in the structure of exports. We are no longer exporting the objects themselves, rather we are only exposing factory functions and interfaces. This allows us to decouple what developers use from our internal implementation. For folks using the fluent chain starting with sp you shouldn't need to update your code. If you are using any of the v1 classes directly you should just need to remove the \"new\" keyword and update the import path. The factory functions signature matches the constructor signature of the v1 objects. // v1 import { Web } from \"@pnp/sp\"; const web: Web = new Web(\"some absolute url\"); const r1 = web(); // v2 import { Web, IWeb } from \"@pnp/sp/webs\"; const web: IWeb = Web(\"some absolute url\"); const r2 = web();","title":"Factory Functions & Interfaces"},{"location":"transition-guide/#extension-methods","text":"Another new capability in v2 is the ability to extend objects and factories. This allows you to easily add methods or properties on a per-object basis. Please see the full article on extension methods describing this great new capability.","title":"Extension Methods"},{"location":"transition-guide/#cdn-publishing","text":"Starting with v2 we will no longer create bundles for each of the packages. Historically these are not commonly used, don't work perfectly for everyone (there are a lot of ways to bundle things), and another piece we need to maintain. Instead we encourage folks to create their own bundles , optimized for their particular scenario. This will result in smaller overall bundle size and allow folks to bundle things to match their scenario. Please review the article on creating your custom bundles to see how to tailor bundles to your needs. The PnPjs bundle will remain, though it is designed only for backwards compatibility and we strongly recommend creating your own bundles, or directly importing the libraries into your projects using selective imports.","title":"CDN publishing"},{"location":"transition-guide/#drop-client-svc-and-sp-taxonomy-libraries","text":"These libraries were created to allow folks to access and manage SharePoint taxonomy and manage metadata. Given that there is upcoming support for taxonomy via a supported REST API we will drop these two libraries. If working with taxonomy remains a core requirement of your application and we do not yet have support for the new apis, please remain on v1 for the time being. As of 2.0.6 we support reading the modern taxonomy API. Docs here","title":"Drop client-svc and sp-taxonomy libraries"},{"location":"authentication/","text":"Authentication \u00b6 One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods. There are two places the PnPjs libraries can be used to connect to various services client (browser) or server . Utility Scenarios \u00b6 BearerTokenFetchClient LambdaFetchClient Client Scenarios \u00b6 SharePoint Framework Connect As: Current User User + AAD App via MSAL User + AAD App via ADAL Connect To: SharePoint as: Current User User + AAD App via MSAL Graph as: Current User User + AAD App via MSAL Both as: Current User User + AAD App via MSAL Single Page Application User + AAD App via MSAL Server Scenarios \u00b6 NodeJS SharePoint App Registration (App-Only) ADAL (App-Only) MSAL (App-Only) - coming soon","title":"Getting Started"},{"location":"authentication/#authentication","text":"One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods. There are two places the PnPjs libraries can be used to connect to various services client (browser) or server .","title":"Authentication"},{"location":"authentication/#utility-scenarios","text":"BearerTokenFetchClient LambdaFetchClient","title":"Utility Scenarios"},{"location":"authentication/#client-scenarios","text":"SharePoint Framework Connect As: Current User User + AAD App via MSAL User + AAD App via ADAL Connect To: SharePoint as: Current User User + AAD App via MSAL Graph as: Current User User + AAD App via MSAL Both as: Current User User + AAD App via MSAL Single Page Application User + AAD App via MSAL","title":"Client Scenarios"},{"location":"authentication/#server-scenarios","text":"NodeJS SharePoint App Registration (App-Only) ADAL (App-Only) MSAL (App-Only) - coming soon","title":"Server Scenarios"},{"location":"authentication/adaljsclient/","text":"@pnp/core/adalclient \u00b6 This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions. Where possible it is recommended to use the MSAL client . Getting Started \u00b6 Install the library and required dependencies npm install @pnp/adaljsclient --save Setup and Use inside SharePoint Framework \u00b6 Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes . This method will only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined below using the constructor to specify the values for an AAD Application you have setup. Calling the graph api \u00b6 By providing the context in the onInit we can create the adal client from known information. import { graph } from \"@pnp/graph\"; import { getRandomString } from \"@pnp/core\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup(this.context); }); } public render(): void { // here we are creating a team with a random name, required Group ReadWrite All permissions const teamName = `ATeam.${getRandomString(4)}`; this.domElement.innerHTML = `Hello, I am creating a team named \"${teamName}\" for you...`; graph.teams.create(teamName, \"This is a description\").then(t => { this.domElement.innerHTML += \"done!\"; }).catch(e => { this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`; }); } Calling the SharePoint API \u00b6 This example shows how to use the ADALClient with the @pnp/sp library to call an API secured with AAD from within SharePoint Framework. import { SPFxAdalClient } from \"@pnp/core\"; import { sp } from \"@pnp/sp/presets/all\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context, sp: { fetchClientFactory: () => new SPFxAdalClient(this.context), }, }); }); } public render(): void { sp.web().then(t => { this.domElement.innerHTML = JSON.stringify(t); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); } Calling the any API \u00b6 You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. import { FetchOptions } from \"@pnp/core\"; import { AdalClient } from \"@pnp/adaljsclient\"; import { ODataDefaultParser } from \"@pnp/queryable\"; // ... public render(): void { // create an ADAL Client const client = AdalClient.fromSPFxContext(this.context); // setup the request options const opts: FetchOptions = { method: \"GET\", headers: { \"Accept\": \"application/json\", }, }; // execute the request client.fetch(\"https://{tenant}.sharepoint.com/_api/web\", opts).then(response => { // create a parser to convert the response into JSON. // You can create your own, at this point you have a fetch Response to work with const parser = new ODataDefaultParser(); parser.parse(response).then(json => { this.domElement.innerHTML = JSON.stringify(json); }); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); } Manually Configure \u00b6 This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD. Setup and Use with Microsoft Graph \u00b6 This sample uses a custom AzureAd app you have created and granted the appropriate permissions. import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph\"; // configure the graph client // parameters are: // client id - the id of the application you created in azure ad // tenant - can be id or URL (shown) // redirect url - absolute url of a page to which your application and Azure AD app allows replies graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"https://myapp/singlesignon.aspx\"); }, }, }); try { // call the graph API const groups = await graph.groups(); console.log(JSON.stringify(groups, null, 4)); } catch (e) { console.error(e); } Nodejs Applications \u00b6 We have a dedicated node client in @pnp/nodejs.","title":"ADAL Client"},{"location":"authentication/adaljsclient/#pnpcoreadalclient","text":"This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions. Where possible it is recommended to use the MSAL client .","title":"@pnp/core/adalclient"},{"location":"authentication/adaljsclient/#getting-started","text":"Install the library and required dependencies npm install @pnp/adaljsclient --save","title":"Getting Started"},{"location":"authentication/adaljsclient/#setup-and-use-inside-sharepoint-framework","text":"Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes . This method will only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined below using the constructor to specify the values for an AAD Application you have setup.","title":"Setup and Use inside SharePoint Framework"},{"location":"authentication/adaljsclient/#calling-the-graph-api","text":"By providing the context in the onInit we can create the adal client from known information. import { graph } from \"@pnp/graph\"; import { getRandomString } from \"@pnp/core\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup(this.context); }); } public render(): void { // here we are creating a team with a random name, required Group ReadWrite All permissions const teamName = `ATeam.${getRandomString(4)}`; this.domElement.innerHTML = `Hello, I am creating a team named \"${teamName}\" for you...`; graph.teams.create(teamName, \"This is a description\").then(t => { this.domElement.innerHTML += \"done!\"; }).catch(e => { this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`; }); }","title":"Calling the graph api"},{"location":"authentication/adaljsclient/#calling-the-sharepoint-api","text":"This example shows how to use the ADALClient with the @pnp/sp library to call an API secured with AAD from within SharePoint Framework. import { SPFxAdalClient } from \"@pnp/core\"; import { sp } from \"@pnp/sp/presets/all\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context, sp: { fetchClientFactory: () => new SPFxAdalClient(this.context), }, }); }); } public render(): void { sp.web().then(t => { this.domElement.innerHTML = JSON.stringify(t); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); }","title":"Calling the SharePoint API"},{"location":"authentication/adaljsclient/#calling-the-any-api","text":"You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. import { FetchOptions } from \"@pnp/core\"; import { AdalClient } from \"@pnp/adaljsclient\"; import { ODataDefaultParser } from \"@pnp/queryable\"; // ... public render(): void { // create an ADAL Client const client = AdalClient.fromSPFxContext(this.context); // setup the request options const opts: FetchOptions = { method: \"GET\", headers: { \"Accept\": \"application/json\", }, }; // execute the request client.fetch(\"https://{tenant}.sharepoint.com/_api/web\", opts).then(response => { // create a parser to convert the response into JSON. // You can create your own, at this point you have a fetch Response to work with const parser = new ODataDefaultParser(); parser.parse(response).then(json => { this.domElement.innerHTML = JSON.stringify(json); }); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); }","title":"Calling the any API"},{"location":"authentication/adaljsclient/#manually-configure","text":"This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD.","title":"Manually Configure"},{"location":"authentication/adaljsclient/#setup-and-use-with-microsoft-graph","text":"This sample uses a custom AzureAd app you have created and granted the appropriate permissions. import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph\"; // configure the graph client // parameters are: // client id - the id of the application you created in azure ad // tenant - can be id or URL (shown) // redirect url - absolute url of a page to which your application and Azure AD app allows replies graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"https://myapp/singlesignon.aspx\"); }, }, }); try { // call the graph API const groups = await graph.groups(); console.log(JSON.stringify(groups, null, 4)); } catch (e) { console.error(e); }","title":"Setup and Use with Microsoft Graph"},{"location":"authentication/adaljsclient/#nodejs-applications","text":"We have a dedicated node client in @pnp/nodejs.","title":"Nodejs Applications"},{"location":"authentication/bearertokenclient/","text":"@pnp/core/BearerTokenFetchClient \u00b6 The BearerTokenFetchClient takes a single parameter representing an access token and uses it to make the requests. The disadvantage to this approach is not knowing to where the request will be sent, which in some cases is fine. An alternative is the LambdaFetchClient Static \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { BearerTokenFetchClient } from \"@pnp/core\"; import { myTokenFactory } from \"./my-auth.js\"; graph.setup({ graph: { fetchClientFactory: () => { // note this method is not async, so your logic here cannot await. // Please see the LambdaFetchClient if you have a need for async support. const token = myTokenFactory(); return new BearerTokenFetchClient(token); }, }, });","title":"Bearer Token Client"},{"location":"authentication/bearertokenclient/#pnpcorebearertokenfetchclient","text":"The BearerTokenFetchClient takes a single parameter representing an access token and uses it to make the requests. The disadvantage to this approach is not knowing to where the request will be sent, which in some cases is fine. An alternative is the LambdaFetchClient","title":"@pnp/core/BearerTokenFetchClient"},{"location":"authentication/bearertokenclient/#static","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { BearerTokenFetchClient } from \"@pnp/core\"; import { myTokenFactory } from \"./my-auth.js\"; graph.setup({ graph: { fetchClientFactory: () => { // note this method is not async, so your logic here cannot await. // Please see the LambdaFetchClient if you have a need for async support. const token = myTokenFactory(); return new BearerTokenFetchClient(token); }, }, });","title":"Static"},{"location":"authentication/client-spa/","text":"Authentication in Single Page Application \u00b6 If you are writing a single page application deployed outside SharePoint it is recommended to use the MSAL client. You can find further details on the settings in the MSAL docs . You will need to ensure that you grant the permissions required to the application you are trying to use. import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); const data = await graph.me();","title":"SPA Auth"},{"location":"authentication/client-spa/#authentication-in-single-page-application","text":"If you are writing a single page application deployed outside SharePoint it is recommended to use the MSAL client. You can find further details on the settings in the MSAL docs . You will need to ensure that you grant the permissions required to the application you are trying to use. import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); const data = await graph.me();","title":"Authentication in Single Page Application"},{"location":"authentication/client-spfx/","text":"Authentication in SharePoint Framework \u00b6 Auth as Current User \u00b6 PnPjs is designed to work as easily as possible within the SharePoint Framework so the authentication setup is very simple for the base case. Supply the current SharePoint Framework context to the library. This works for both SharePoint authentication and Graph authentication using the current user. Graph permissions are controlled by the permissions granted to the SharePoint shared application within your tenant. The below example is taken from a SharePoint Framework webpart. Connect to SharePoint as Current User \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ... Connect to Graph as Current User \u00b6 Permissions for this graph connection are controlled by the Shared SharePoint Application. You can target other applications using the MSAL Client . import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present // this will use the ADAL client behind the scenes with no additional configuration work graph.setup(this.context); } // ... MSAL Client \u00b6 You might want/need to use a client configured to use your own AAD application and not the shared SharePoint application. You can do so using the MSAL client . Here we show this using graph, this works the same with any of the setup strategies . Please see the MSAL library docs for more details on what values to supply in the configuration. Note: you must install the @pnp/msaljsclient client package before using it import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); } // ... ADAL Client \u00b6 You can use the ADAL client from within SPFx, though it is recommended to transition to the MSAL client. Note: you must install the @pnp/adaljsclient client package before using it import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"\"); }, }); } // ...","title":"SPFx Auth"},{"location":"authentication/client-spfx/#authentication-in-sharepoint-framework","text":"","title":"Authentication in SharePoint Framework"},{"location":"authentication/client-spfx/#auth-as-current-user","text":"PnPjs is designed to work as easily as possible within the SharePoint Framework so the authentication setup is very simple for the base case. Supply the current SharePoint Framework context to the library. This works for both SharePoint authentication and Graph authentication using the current user. Graph permissions are controlled by the permissions granted to the SharePoint shared application within your tenant. The below example is taken from a SharePoint Framework webpart.","title":"Auth as Current User"},{"location":"authentication/client-spfx/#connect-to-sharepoint-as-current-user","text":"import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ...","title":"Connect to SharePoint as Current User"},{"location":"authentication/client-spfx/#connect-to-graph-as-current-user","text":"Permissions for this graph connection are controlled by the Shared SharePoint Application. You can target other applications using the MSAL Client . import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present // this will use the ADAL client behind the scenes with no additional configuration work graph.setup(this.context); } // ...","title":"Connect to Graph as Current User"},{"location":"authentication/client-spfx/#msal-client","text":"You might want/need to use a client configured to use your own AAD application and not the shared SharePoint application. You can do so using the MSAL client . Here we show this using graph, this works the same with any of the setup strategies . Please see the MSAL library docs for more details on what values to supply in the configuration. Note: you must install the @pnp/msaljsclient client package before using it import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); } // ...","title":"MSAL Client"},{"location":"authentication/client-spfx/#adal-client","text":"You can use the ADAL client from within SPFx, though it is recommended to transition to the MSAL client. Note: you must install the @pnp/adaljsclient client package before using it import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"\"); }, }); } // ...","title":"ADAL Client"},{"location":"authentication/lambdaclient/","text":"@pnp/core/LambdaFetchClient \u00b6 The LambdaFetchClient class allows you to provide an async function that returns an access token using any logic/supporting libraries you need. This provides total freedom to define how you do authentication, so long as it results in a usable Bearer token to call the target resource. The advantage to the LambdaFetchClient is that you get the url for each request, meaning your logic can account for where the request is headed. The token function should be as efficient as possible as it's logic must complete before each request will be sent. Signature \u00b6 The LambdaFetchClient accepts a single argument of type ILambdaTokenFactoryParams. // signature of method, the return string is the access token (parms: ILambdaTokenFactoryParams) => Promise // ILambdaTokenFactoryParams export interface ILambdaTokenFactoryParams { /** * Url to which the request for which we are requesting a token will be sent */ url: string; /** * Any options supplied for the request */ options: IFetchOptions; } @azure/msal-browser example \u00b6 This example shows how to use @azure/msal-browser along with LambdaFetchClient to achieve signin. msal-browser has many possible configurations which are described within their documentation. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { LambdaFetchClient } from \"@pnp/core\"; import { PublicClientApplication, Configuration } from \"@azure/msal-browser\"; const config: Configuration = { auth: { clientId: \"{client id}\", authority: \"https://login.microsoftonline.com/common/\" } } // create a single application, could also create this within the lambda client, but it would create a new applicaiton per request const msal = new PublicClientApplication(config); // create a new instance of the lambda fetch client const client = new LambdaFetchClient(async () => { const request = { scopes: [\"User.Read.All\"], }; const response = await msal.loginPopup(request); // lamba returns the access token return response.accessToken; }); // setup graph with the client graph.setup({ graph: { fetchClientFactory: () => client, }, }); // execute the request to graph which will use the client defined above const result = await graph.users();","title":"Lambda Token Client"},{"location":"authentication/lambdaclient/#pnpcorelambdafetchclient","text":"The LambdaFetchClient class allows you to provide an async function that returns an access token using any logic/supporting libraries you need. This provides total freedom to define how you do authentication, so long as it results in a usable Bearer token to call the target resource. The advantage to the LambdaFetchClient is that you get the url for each request, meaning your logic can account for where the request is headed. The token function should be as efficient as possible as it's logic must complete before each request will be sent.","title":"@pnp/core/LambdaFetchClient"},{"location":"authentication/lambdaclient/#signature","text":"The LambdaFetchClient accepts a single argument of type ILambdaTokenFactoryParams. // signature of method, the return string is the access token (parms: ILambdaTokenFactoryParams) => Promise // ILambdaTokenFactoryParams export interface ILambdaTokenFactoryParams { /** * Url to which the request for which we are requesting a token will be sent */ url: string; /** * Any options supplied for the request */ options: IFetchOptions; }","title":"Signature"},{"location":"authentication/lambdaclient/#azuremsal-browser-example","text":"This example shows how to use @azure/msal-browser along with LambdaFetchClient to achieve signin. msal-browser has many possible configurations which are described within their documentation. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { LambdaFetchClient } from \"@pnp/core\"; import { PublicClientApplication, Configuration } from \"@azure/msal-browser\"; const config: Configuration = { auth: { clientId: \"{client id}\", authority: \"https://login.microsoftonline.com/common/\" } } // create a single application, could also create this within the lambda client, but it would create a new applicaiton per request const msal = new PublicClientApplication(config); // create a new instance of the lambda fetch client const client = new LambdaFetchClient(async () => { const request = { scopes: [\"User.Read.All\"], }; const response = await msal.loginPopup(request); // lamba returns the access token return response.accessToken; }); // setup graph with the client graph.setup({ graph: { fetchClientFactory: () => client, }, }); // execute the request to graph which will use the client defined above const result = await graph.users();","title":"@azure/msal-browser example"},{"location":"authentication/msaljsclient/","text":"msaljsclient - MSAL Client for PnPjs \u00b6 The MSAL client is a thin wrapper around the MSAL library adapting it for use with PnPjs's request pipeline. Install \u00b6 You need to install the MSAL client before using it. This is in addition to installing the other PnPjs libraries you require. npm install @pnp/msaljsclient --save Configure \u00b6 The PnP client is a very thin wrapper around the MSAL library and you can supply any of the arguments supported. These are described in the MSAL docs . The basic configuration values you need (at least from our testing) are client id, authority, and redirectUri. The other options are settable but not required. This article is not intended to be an exhaustive discussion of all the MSAL configuration possibilities, please see the official docs to understand all of the available options. The second parameter when configuring the PnP client is the list of scope you are seeking to use. These must be configured and properly granted within AAD and you can request one or more scopes as needed for the current scenario. Use in SPFx \u00b6 Calling SharePoint via MSAL \u00b6 When calling the SharePoint REST API we must use only a special scope \"https://{tenant}.sharepoint.com/.default\" import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/mytentant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://mytentant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"https://mytentant.sharepoint.com/.default\"]), }, }); const r = await sp.web(); Calling Graph via MSAL \u00b6 When calling the graph API you must specify the scopes you need and ensure they are configured in AAD import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups(); Use in Single Page Applications \u00b6 You can also use the PnPjs MSAL client within your SPA applications. Please review the various settings to ensure you are configuring MSAL as needed for your application import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://myapp.com/login.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups(); Get a Token \u00b6 You can also use the client to get a token if you need a token for use outside the PnPjs libraries import { MsalClient } from \"@pnp/msaljsclient\"; // note we do not provide scopes here as the second parameter. We certainly could and will get a token // based on those scopes by making a call to getToken() without a param. const client = new MsalClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant}.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://{tenant}.sharepoint.com/sites/dev/SitePages/webpacktest.aspx\", }, }); const token = await client.getToken([\"Group.Read.All\"]); const token2 = await client.getToken([\"Files.Read\"]);","title":"MSAL Client"},{"location":"authentication/msaljsclient/#msaljsclient-msal-client-for-pnpjs","text":"The MSAL client is a thin wrapper around the MSAL library adapting it for use with PnPjs's request pipeline.","title":"msaljsclient - MSAL Client for PnPjs"},{"location":"authentication/msaljsclient/#install","text":"You need to install the MSAL client before using it. This is in addition to installing the other PnPjs libraries you require. npm install @pnp/msaljsclient --save","title":"Install"},{"location":"authentication/msaljsclient/#configure","text":"The PnP client is a very thin wrapper around the MSAL library and you can supply any of the arguments supported. These are described in the MSAL docs . The basic configuration values you need (at least from our testing) are client id, authority, and redirectUri. The other options are settable but not required. This article is not intended to be an exhaustive discussion of all the MSAL configuration possibilities, please see the official docs to understand all of the available options. The second parameter when configuring the PnP client is the list of scope you are seeking to use. These must be configured and properly granted within AAD and you can request one or more scopes as needed for the current scenario.","title":"Configure"},{"location":"authentication/msaljsclient/#use-in-spfx","text":"","title":"Use in SPFx"},{"location":"authentication/msaljsclient/#calling-sharepoint-via-msal","text":"When calling the SharePoint REST API we must use only a special scope \"https://{tenant}.sharepoint.com/.default\" import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/mytentant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://mytentant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"https://mytentant.sharepoint.com/.default\"]), }, }); const r = await sp.web();","title":"Calling SharePoint via MSAL"},{"location":"authentication/msaljsclient/#calling-graph-via-msal","text":"When calling the graph API you must specify the scopes you need and ensure they are configured in AAD import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups();","title":"Calling Graph via MSAL"},{"location":"authentication/msaljsclient/#use-in-single-page-applications","text":"You can also use the PnPjs MSAL client within your SPA applications. Please review the various settings to ensure you are configuring MSAL as needed for your application import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://myapp.com/login.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups();","title":"Use in Single Page Applications"},{"location":"authentication/msaljsclient/#get-a-token","text":"You can also use the client to get a token if you need a token for use outside the PnPjs libraries import { MsalClient } from \"@pnp/msaljsclient\"; // note we do not provide scopes here as the second parameter. We certainly could and will get a token // based on those scopes by making a call to getToken() without a param. const client = new MsalClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant}.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://{tenant}.sharepoint.com/sites/dev/SitePages/webpacktest.aspx\", }, }); const token = await client.getToken([\"Group.Read.All\"]); const token2 = await client.getToken([\"Files.Read\"]);","title":"Get a Token"},{"location":"authentication/server-nodejs/","text":"Authentication in Nodejs \u00b6 SharePoint App Registration \u00b6 Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Auth . Within the PnPjs testing framework we make use of SharePoint App Registration. This uses the SPFetchClient client from the nodejs package. This client works based on the legacy SharePoint App Registration model making use of a client and secret granted permissions through AppInv.aspx. This method works and at the time of writing has no published end date. See: details on how to register a legacy SharePoint application . import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web(); MSAL \u00b6 Added in 2.0.11 You can now use the @azure/msal-node client with PnPjs using MsalFetchClient. You must configure an AAD application with the appropriate permissions for your application. At the time this article was written the msal-node package is not yet GA. Call Graph \u00b6 You can call the Microsoft Graph API with a client id and secret or certificate (see SharePoint example for cert auth) import { graph } from \"@pnp/graph/presets/all\"; // configure your node options graph.setup({ graph: { fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientId: \"{guid}\", clientSecret: \"{client secret}\", } }); }, }, }); const userInfo = await graph.users(); Call SharePoint \u00b6 To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below. mkdir \\temp cd \\temp openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle' openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass Using the above code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration. You need to set the baseUrl property when using the MsalFetchClient import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const w = await sp.web(); ADAL \u00b6 The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. See: More details on the node client import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"NodeJS Auth"},{"location":"authentication/server-nodejs/#authentication-in-nodejs","text":"","title":"Authentication in Nodejs"},{"location":"authentication/server-nodejs/#sharepoint-app-registration","text":"Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Auth . Within the PnPjs testing framework we make use of SharePoint App Registration. This uses the SPFetchClient client from the nodejs package. This client works based on the legacy SharePoint App Registration model making use of a client and secret granted permissions through AppInv.aspx. This method works and at the time of writing has no published end date. See: details on how to register a legacy SharePoint application . import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web();","title":"SharePoint App Registration"},{"location":"authentication/server-nodejs/#msal","text":"Added in 2.0.11 You can now use the @azure/msal-node client with PnPjs using MsalFetchClient. You must configure an AAD application with the appropriate permissions for your application. At the time this article was written the msal-node package is not yet GA.","title":"MSAL"},{"location":"authentication/server-nodejs/#call-graph","text":"You can call the Microsoft Graph API with a client id and secret or certificate (see SharePoint example for cert auth) import { graph } from \"@pnp/graph/presets/all\"; // configure your node options graph.setup({ graph: { fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientId: \"{guid}\", clientSecret: \"{client secret}\", } }); }, }, }); const userInfo = await graph.users();","title":"Call Graph"},{"location":"authentication/server-nodejs/#call-sharepoint","text":"To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below. mkdir \\temp cd \\temp openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle' openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass Using the above code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration. You need to set the baseUrl property when using the MsalFetchClient import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const w = await sp.web();","title":"Call SharePoint"},{"location":"authentication/server-nodejs/#adal","text":"The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. See: More details on the node client import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"ADAL"},{"location":"authentication/sp-app-registration/","text":"Legacy SharePoint App Registration \u00b6 This section outlines how to register for a client id and secret for use in the above code. Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Authentication . Register An Add-In \u00b6 Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article. Grant Your Add-In Permissions \u00b6 Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the App Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note that the above XML will grant full tenant control. This is OK for testing, but you should grant only those permissions necessary for your application in production.","title":"SP App Reg"},{"location":"authentication/sp-app-registration/#legacy-sharepoint-app-registration","text":"This section outlines how to register for a client id and secret for use in the above code. Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Authentication .","title":"Legacy SharePoint App Registration"},{"location":"authentication/sp-app-registration/#register-an-add-in","text":"Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.","title":"Register An Add-In"},{"location":"authentication/sp-app-registration/#grant-your-add-in-permissions","text":"Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the App Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note that the above XML will grant full tenant control. This is OK for testing, but you should grant only those permissions necessary for your application in production.","title":"Grant Your Add-In Permissions"},{"location":"common/","text":"@pnp/core \u00b6 The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/core --save Import and use functionality, see details on modules below. import { getGUID } from \"@pnp/core\"; console.log(getGUID()); Exports \u00b6 collections libconfig netutil storage util Custom HttpClient","title":"common"},{"location":"common/#pnpcore","text":"The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well.","title":"@pnp/core"},{"location":"common/#getting-started","text":"Install the library and required dependencies npm install @pnp/core --save Import and use functionality, see details on modules below. import { getGUID } from \"@pnp/core\"; console.log(getGUID());","title":"Getting Started"},{"location":"common/#exports","text":"collections libconfig netutil storage util Custom HttpClient","title":"Exports"},{"location":"common/collections/","text":"@pnp/core/collections \u00b6 The collections module provides typings and classes related to working with dictionaries. TypedHash \u00b6 Interface used to described an object with string keys corresponding to values of type T export interface TypedHash { [key: string]: T; } objectToMap \u00b6 Converts a plain object to a Map instance const map = objectToMap({ a: \"b\", c: \"d\"}); mergeMaps \u00b6 Merges two or more maps, overwriting values with the same key. Last value in wins. const m1 = new Map(); const m2 = new Map(); const m3 = new Map(); const m4 = new Map(); const m = mergeMaps(m1, m2, m3, m4);","title":"collections"},{"location":"common/collections/#pnpcorecollections","text":"The collections module provides typings and classes related to working with dictionaries.","title":"@pnp/core/collections"},{"location":"common/collections/#typedhash","text":"Interface used to described an object with string keys corresponding to values of type T export interface TypedHash { [key: string]: T; }","title":"TypedHash"},{"location":"common/collections/#objecttomap","text":"Converts a plain object to a Map instance const map = objectToMap({ a: \"b\", c: \"d\"});","title":"objectToMap"},{"location":"common/collections/#mergemaps","text":"Merges two or more maps, overwriting values with the same key. Last value in wins. const m1 = new Map(); const m2 = new Map(); const m3 = new Map(); const m4 = new Map(); const m = mergeMaps(m1, m2, m3, m4);","title":"mergeMaps"},{"location":"common/custom-httpclientimpl/","text":"Custom HttpClientImpl \u00b6 This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation. It is possible you may need complete control over the sending and receiving of requests. Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation. The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. export interface HttpClientImpl { fetch(url: string, options: FetchOptions): Promise; } There is a single method \"fetch\" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. export interface FetchOptions { method?: string; headers?: HeadersInit | { [index: string]: string }; body?: BodyInit; mode?: string | RequestMode; credentials?: string | RequestCredentials; cache?: string | RequestCache; } So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read \ud83d\udc4d. Using Your Custom HttpClientImpl \u00b6 Once you have written your implementation using it on your requests is done by setting it in the global library configuration: import { setup } from \"@pnp/core\"; import { sp, Web } from \"@pnp/sp\"; import { MyAwesomeClient } from \"./awesomeclient\"; sp.setup({ sp: { fetchClientFactory: () => { return new MyAwesomeClient(); } } }); let w = new Web(\"{site url}\"); // this request will use your client. const result = await w.select(\"Title\")(); console.log(result); Subclassing is Better \u00b6 You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation. A FINAL NOTE \u00b6 Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.","title":"Custom HttpClientImpl"},{"location":"common/custom-httpclientimpl/#custom-httpclientimpl","text":"This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation. It is possible you may need complete control over the sending and receiving of requests. Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation. The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. export interface HttpClientImpl { fetch(url: string, options: FetchOptions): Promise; } There is a single method \"fetch\" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. export interface FetchOptions { method?: string; headers?: HeadersInit | { [index: string]: string }; body?: BodyInit; mode?: string | RequestMode; credentials?: string | RequestCredentials; cache?: string | RequestCache; } So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read \ud83d\udc4d.","title":"Custom HttpClientImpl"},{"location":"common/custom-httpclientimpl/#using-your-custom-httpclientimpl","text":"Once you have written your implementation using it on your requests is done by setting it in the global library configuration: import { setup } from \"@pnp/core\"; import { sp, Web } from \"@pnp/sp\"; import { MyAwesomeClient } from \"./awesomeclient\"; sp.setup({ sp: { fetchClientFactory: () => { return new MyAwesomeClient(); } } }); let w = new Web(\"{site url}\"); // this request will use your client. const result = await w.select(\"Title\")(); console.log(result);","title":"Using Your Custom HttpClientImpl"},{"location":"common/custom-httpclientimpl/#subclassing-is-better","text":"You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation.","title":"Subclassing is Better"},{"location":"common/custom-httpclientimpl/#a-final-note","text":"Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.","title":"A FINAL NOTE"},{"location":"common/libconfig/","text":"@pnp/core/libconfig \u00b6 Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications. ILibraryConfiguration Interface \u00b6 Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below export interface ILibraryConfiguration { /** * Allows caching to be global disabled, default: false */ globalCacheDisable?: boolean; /** * Defines the default store used by the usingCaching method, default: session */ defaultCachingStore?: \"session\" | \"local\"; /** * Defines the default timeout in seconds used by the usingCaching method, default 30 */ defaultCachingTimeoutSeconds?: number; /** * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval */ enableCacheExpiration?: boolean; /** * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) */ cacheExpirationIntervalMilliseconds?: number; /** * Used to supply the current context from an SPFx webpart to the library */ spfxContext?: any; } RuntimeConfigImpl \u00b6 The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method. assign \u00b6 The assign method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below \"Using RuntimeConfig within your application\". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. import { RuntimeConfig } from \"@pnp/core\"; // add your custom keys to the global configuration // note you can use object hashes as values RuntimeConfig.assign({ \"myKey1\": \"value 1\", \"myKey2\": { \"subKey\": \"sub value 1\", \"subKey2\": \"sub value 2\", }, }); // read your custom values const v = RuntimeConfig.get(\"myKey1\"); // \"value 1\" Using RuntimeConfig within your Application \u00b6 If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties import { ILibraryConfiguration, RuntimeConfig, ITypedHash } from \"@pnp/core\"; // first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because // TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. // create the interface of your properties // by creating this separately you allows others to compose your parts into their own config interface MyConfigurationPart { // you can create a grouped definition and access your settings as an object // keys can be optional or required as defined by your interface my?: { prop1?: string; prop2?: string; } // and/or define multiple top level properties (beware key collision) // it is good practice to use a unique prefix myProp1: string; myProp2: number; } // now create a combined interface interface MyConfiguration extends ILibraryConfiguration, MyConfigurationPart { } // now create a wrapper object and expose your properties class MyRuntimeConfigImpl { // exposing a nested property public get prop1(): ITypedHash { const myPart = RuntimeConfig.get(\"my\"); if (myPart !== null && typeof myPart !== \"undefined\" && typeof myPart.prop1 !== \"undefined\") { return myPart.prop1; } return {}; } // exposing a root level property public get myProp1(): string | null { let myProp1 = RuntimeConfig.get(\"myProp1\"); if (myProp1 === null) { myProp1 = \"some default value\"; } return myProp1; } setup(config: MyConfiguration): void { RuntimeConfig.assign(config); } } // create a single static instance of your impl class export let MyRuntimeConfig = new MyRuntimeConfigImpl(); Now in other files you can use and set your configuration with a typed interface and properties import { MyRuntimeConfig } from \"{location of module}\"; MyRuntimeConfig.setup({ my: { prop1: \"hello\", }, }); const value = MyRuntimeConfig.myProp1; // \"hello\"","title":"libconfig"},{"location":"common/libconfig/#pnpcorelibconfig","text":"Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications.","title":"@pnp/core/libconfig"},{"location":"common/libconfig/#ilibraryconfiguration-interface","text":"Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below export interface ILibraryConfiguration { /** * Allows caching to be global disabled, default: false */ globalCacheDisable?: boolean; /** * Defines the default store used by the usingCaching method, default: session */ defaultCachingStore?: \"session\" | \"local\"; /** * Defines the default timeout in seconds used by the usingCaching method, default 30 */ defaultCachingTimeoutSeconds?: number; /** * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval */ enableCacheExpiration?: boolean; /** * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) */ cacheExpirationIntervalMilliseconds?: number; /** * Used to supply the current context from an SPFx webpart to the library */ spfxContext?: any; }","title":"ILibraryConfiguration Interface"},{"location":"common/libconfig/#runtimeconfigimpl","text":"The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method.","title":"RuntimeConfigImpl"},{"location":"common/libconfig/#assign","text":"The assign method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below \"Using RuntimeConfig within your application\". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. import { RuntimeConfig } from \"@pnp/core\"; // add your custom keys to the global configuration // note you can use object hashes as values RuntimeConfig.assign({ \"myKey1\": \"value 1\", \"myKey2\": { \"subKey\": \"sub value 1\", \"subKey2\": \"sub value 2\", }, }); // read your custom values const v = RuntimeConfig.get(\"myKey1\"); // \"value 1\"","title":"assign"},{"location":"common/libconfig/#using-runtimeconfig-within-your-application","text":"If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties import { ILibraryConfiguration, RuntimeConfig, ITypedHash } from \"@pnp/core\"; // first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because // TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. // create the interface of your properties // by creating this separately you allows others to compose your parts into their own config interface MyConfigurationPart { // you can create a grouped definition and access your settings as an object // keys can be optional or required as defined by your interface my?: { prop1?: string; prop2?: string; } // and/or define multiple top level properties (beware key collision) // it is good practice to use a unique prefix myProp1: string; myProp2: number; } // now create a combined interface interface MyConfiguration extends ILibraryConfiguration, MyConfigurationPart { } // now create a wrapper object and expose your properties class MyRuntimeConfigImpl { // exposing a nested property public get prop1(): ITypedHash { const myPart = RuntimeConfig.get(\"my\"); if (myPart !== null && typeof myPart !== \"undefined\" && typeof myPart.prop1 !== \"undefined\") { return myPart.prop1; } return {}; } // exposing a root level property public get myProp1(): string | null { let myProp1 = RuntimeConfig.get(\"myProp1\"); if (myProp1 === null) { myProp1 = \"some default value\"; } return myProp1; } setup(config: MyConfiguration): void { RuntimeConfig.assign(config); } } // create a single static instance of your impl class export let MyRuntimeConfig = new MyRuntimeConfigImpl(); Now in other files you can use and set your configuration with a typed interface and properties import { MyRuntimeConfig } from \"{location of module}\"; MyRuntimeConfig.setup({ my: { prop1: \"hello\", }, }); const value = MyRuntimeConfig.myProp1; // \"hello\"","title":"Using RuntimeConfig within your Application"},{"location":"common/netutil/","text":"@pnp/core/net \u00b6 This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces are described below (many have no use outside the library) as well as several classes. Interfaces \u00b6 HttpClientImpl \u00b6 Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method \"fetch\" takes a URL and options. It returns a Promise . Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed. RequestClient \u00b6 An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the underlying HttpClientImpl fetch method. Classes \u00b6 This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl. FetchClient \u00b6 Basic implementation that calls the global (window) fetch method with no additional processing. import { FetchClient } from \"@pnp/core\"; const client = new FetchClient(); client.fetch(\"{url}\", {}); BearerTokenFetchClient \u00b6 A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string. import { BearerTokenFetchClient } from \"@pnp/core\"; const client = new BearerTokenFetchClient(\"{authentication token}\"); client.fetch(\"{url}\", {});","title":"netutil"},{"location":"common/netutil/#pnpcorenet","text":"This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces are described below (many have no use outside the library) as well as several classes.","title":"@pnp/core/net"},{"location":"common/netutil/#interfaces","text":"","title":"Interfaces"},{"location":"common/netutil/#httpclientimpl","text":"Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method \"fetch\" takes a URL and options. It returns a Promise . Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed.","title":"HttpClientImpl"},{"location":"common/netutil/#requestclient","text":"An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the underlying HttpClientImpl fetch method.","title":"RequestClient"},{"location":"common/netutil/#classes","text":"This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl.","title":"Classes"},{"location":"common/netutil/#fetchclient","text":"Basic implementation that calls the global (window) fetch method with no additional processing. import { FetchClient } from \"@pnp/core\"; const client = new FetchClient(); client.fetch(\"{url}\", {});","title":"FetchClient"},{"location":"common/netutil/#bearertokenfetchclient","text":"A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string. import { BearerTokenFetchClient } from \"@pnp/core\"; const client = new BearerTokenFetchClient(\"{authentication token}\"); client.fetch(\"{url}\", {});","title":"BearerTokenFetchClient"},{"location":"common/storage/","text":"@pnp/core/storage \u00b6 This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below. PnPClientStorage \u00b6 The main export of this module, contains properties representing local and session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); const myvalue = storage.local.get(\"mykey\"); PnPClientStorageWrapper \u00b6 Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // get a value from storage const value = storage.local.get(\"mykey\"); // put a value into storage storage.local.put(\"mykey2\", \"my value\"); // put a value into storage with an expiration storage.local.put(\"mykey2\", \"my value\", new Date()); // put a simple object into storage // because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects storage.local.put(\"mykey3\", { key: \"value\", key2: \"value2\", }); // remove a value from storage storage.local.delete(\"mykey3\"); // get an item or add it if it does not exist // returns a promise in case you need time to get the value for storage // optionally takes a third parameter specifying the expiration storage.local.getOrPut(\"mykey4\", () => { return Promise.resolve(\"value\"); }); // delete expired items storage.local.deleteExpired(); Cache Expiration \u00b6 The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // session storage storage.session.deleteExpired(); // local storage storage.local.deleteExpired(); // this returns a promise, so you can perform some activity after the expired items are removed: storage.local.deleteExpired().then(_ => { // init my application }); The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. import { setup } from \"@pnp/core\"; setup({ enableCacheExpiration: true, cacheExpirationIntervalMilliseconds: 1000, // optional });","title":"storage"},{"location":"common/storage/#pnpcorestorage","text":"This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.","title":"@pnp/core/storage"},{"location":"common/storage/#pnpclientstorage","text":"The main export of this module, contains properties representing local and session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); const myvalue = storage.local.get(\"mykey\");","title":"PnPClientStorage"},{"location":"common/storage/#pnpclientstoragewrapper","text":"Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // get a value from storage const value = storage.local.get(\"mykey\"); // put a value into storage storage.local.put(\"mykey2\", \"my value\"); // put a value into storage with an expiration storage.local.put(\"mykey2\", \"my value\", new Date()); // put a simple object into storage // because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects storage.local.put(\"mykey3\", { key: \"value\", key2: \"value2\", }); // remove a value from storage storage.local.delete(\"mykey3\"); // get an item or add it if it does not exist // returns a promise in case you need time to get the value for storage // optionally takes a third parameter specifying the expiration storage.local.getOrPut(\"mykey4\", () => { return Promise.resolve(\"value\"); }); // delete expired items storage.local.deleteExpired();","title":"PnPClientStorageWrapper"},{"location":"common/storage/#cache-expiration","text":"The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // session storage storage.session.deleteExpired(); // local storage storage.local.deleteExpired(); // this returns a promise, so you can perform some activity after the expired items are removed: storage.local.deleteExpired().then(_ => { // init my application }); The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. import { setup } from \"@pnp/core\"; setup({ enableCacheExpiration: true, cacheExpirationIntervalMilliseconds: 1000, // optional });","title":"Cache Expiration"},{"location":"common/util/","text":"@pnp/core/util \u00b6 This module contains utility methods that you can import individually from the common library. import { getRandomString, } from \"@pnp/core\"; // use from individually imported method console.log(getRandomString(10)); assign \u00b6 Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing properties. import { assign } from \"@pnp/core\"; let obj1 = { prop: 1, prop2: 2, }; const obj2 = { prop: 4, prop3: 9, }; const example1 = assign(obj1, obj2); // example1 = { prop: 4, prop2: 2, prop3: 9 } //noOverwrite = true stops overwriting existing properties const example2 = assign(obj1, obj2, true); // example2 = { prop: 1, prop2: 2, prop3: 9 } combine \u00b6 Combines any number of paths, normalizing the slashes as required import { combine } from \"@pnp/core\"; // \"https://microsoft.com/something/more\" const paths = combine(\"https://microsoft.com\", \"something\", \"more\"); // \"also/works/with/relative\" const paths2 = combine(\"/also/\", \"/works\", \"with/\", \"/relative\\\\\"); dateAdd \u00b6 Manipulates a date, please see the Stack Overflow discussion from where this method was taken. import { dateAdd } from \"@pnp/core\"; const testDate = new Date(); dateAdd(testDate,'minute',10); getCtxCallback \u00b6 Gets a callback function which will maintain context across async calls. import { getCtxCallback } from \"@pnp/core\"; const contextThis = { myProp: 6, }; function theFunction() { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp; } const callback = getCtxCallback(contextThis, theFunction); callback(); // returns 6 // You can also supply additional parameters if needed function theFunction2(g: number) { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp + g; } const callback2 = getCtxCallback(contextThis, theFunction2, 4); callback2(); // returns 10 (6 + 4) getGUID \u00b6 Creates a random guid, please see the Stack Overflow discussion from where this method was taken. import { getGUID } from \"@pnp/core\"; const newGUID = getGUID(); getRandomString \u00b6 Gets a random string consisting of the number of characters requested. import { getRandomString } from \"@pnp/core\"; const randomString = getRandomString(10); hOP \u00b6 Shortcut for Object.hasOwnProperty. Determines if an object has a specified property. import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { //Checks to see if the error object has a property called isHttpRequestError. Returns a bool. if (hOP(e, \"isHttpRequestError\")) { // Handle this type or error } else { // not an HttpRequestError so we do something else } } isArray \u00b6 Determines if a supplied variable represents an array. import { isArray } from \"@pnp/core\"; let x:String[] = [1,2,3]]; if (isArray(x)){ console.log(\"I am an array\"); }else{ console.log(\"I am not an array\"); } isFunc \u00b6 Determines if a supplied variable represents a function. import { isFunc } from \"@pnp/core\"; public testFunction() { console.log(\"test function\"); return } if (isFunc(testFunction)){ console.log(\"this is a function\"); testFunction(); } isUrlAbsolute \u00b6 Determines if a supplied url is absolute and returns true; otherwise returns false. import { isUrlAbsolute } from \"@pnp/core\"; const webPath = 'https://{tenant}.sharepoint.com/sites/dev/'; if (isUrlAbsolute(webPath)){ console.log(\"URL is absolute\"); }else{ console.log(\"URL is not absolute\"); } objectDefinedNotNull \u00b6 Determines if an object is defined and not null. import { objectDefinedNotNull } from \"@pnp/core\"; let obj = { prop: 1 }; if (objectDefinedNotNull(obj)){ console.log(\"Not null\"); }else{ console.log(\"Null\"); } stringIsNullOrEmpty \u00b6 Determines if a supplied string is null or empty. import { stringIsNullOrEmpty } from \"@pnp/core\"; let x:String = \"hello\"; if (stringIsNullOrEmpty(x)){ console.log(\"Null or empty\"); }else{ console.log(\"Not null or empty\"); } Removed \u00b6 Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods below for use in your projects should you require. /** * Loads a stylesheet into the current page * * @param path The url to the stylesheet * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues */ public static loadStylesheet(path: string, avoidCache: boolean): void { if (avoidCache) { path += \"?\" + encodeURIComponent((new Date()).getTime().toString()); } const head = document.getElementsByTagName(\"head\"); if (head.length > 0) { const e = document.createElement(\"link\"); head[0].appendChild(e); e.setAttribute(\"type\", \"text/css\"); e.setAttribute(\"rel\", \"stylesheet\"); e.setAttribute(\"href\", path); } } /** * Tests if a url param exists * * @param name The name of the url parameter to check */ public static urlParamExists(name: string): boolean { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); return regex.test(location.search); } /** * Gets a url param value by name * * @param name The name of the parameter for which we want the value */ public static getUrlParamByName(name: string): string { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); const results = regex.exec(location.search); return results == null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \")); } /** * Gets a url param by name and attempts to parse a bool value * * @param name The name of the parameter for which we want the boolean value */ public static getUrlParamBoolByName(name: string): boolean { const p = this.getUrlParamByName(name); const isFalse = (p === \"\" || /false|0/i.test(p)); return !isFalse; } /** * Inserts the string s into the string target as the index specified by index * * @param target The string into which we will insert s * @param index The location in target to insert s (zero based) * @param s The string to insert into target at position index */ public static stringInsert(target: string, index: number, s: string): string { if (index > 0) { return target.substring(0, index) + s + target.substring(index, target.length); } return s + target; }","title":"util"},{"location":"common/util/#pnpcoreutil","text":"This module contains utility methods that you can import individually from the common library. import { getRandomString, } from \"@pnp/core\"; // use from individually imported method console.log(getRandomString(10));","title":"@pnp/core/util"},{"location":"common/util/#assign","text":"Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing properties. import { assign } from \"@pnp/core\"; let obj1 = { prop: 1, prop2: 2, }; const obj2 = { prop: 4, prop3: 9, }; const example1 = assign(obj1, obj2); // example1 = { prop: 4, prop2: 2, prop3: 9 } //noOverwrite = true stops overwriting existing properties const example2 = assign(obj1, obj2, true); // example2 = { prop: 1, prop2: 2, prop3: 9 }","title":"assign"},{"location":"common/util/#combine","text":"Combines any number of paths, normalizing the slashes as required import { combine } from \"@pnp/core\"; // \"https://microsoft.com/something/more\" const paths = combine(\"https://microsoft.com\", \"something\", \"more\"); // \"also/works/with/relative\" const paths2 = combine(\"/also/\", \"/works\", \"with/\", \"/relative\\\\\");","title":"combine"},{"location":"common/util/#dateadd","text":"Manipulates a date, please see the Stack Overflow discussion from where this method was taken. import { dateAdd } from \"@pnp/core\"; const testDate = new Date(); dateAdd(testDate,'minute',10);","title":"dateAdd"},{"location":"common/util/#getctxcallback","text":"Gets a callback function which will maintain context across async calls. import { getCtxCallback } from \"@pnp/core\"; const contextThis = { myProp: 6, }; function theFunction() { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp; } const callback = getCtxCallback(contextThis, theFunction); callback(); // returns 6 // You can also supply additional parameters if needed function theFunction2(g: number) { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp + g; } const callback2 = getCtxCallback(contextThis, theFunction2, 4); callback2(); // returns 10 (6 + 4)","title":"getCtxCallback"},{"location":"common/util/#getguid","text":"Creates a random guid, please see the Stack Overflow discussion from where this method was taken. import { getGUID } from \"@pnp/core\"; const newGUID = getGUID();","title":"getGUID"},{"location":"common/util/#getrandomstring","text":"Gets a random string consisting of the number of characters requested. import { getRandomString } from \"@pnp/core\"; const randomString = getRandomString(10);","title":"getRandomString"},{"location":"common/util/#hop","text":"Shortcut for Object.hasOwnProperty. Determines if an object has a specified property. import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { //Checks to see if the error object has a property called isHttpRequestError. Returns a bool. if (hOP(e, \"isHttpRequestError\")) { // Handle this type or error } else { // not an HttpRequestError so we do something else } }","title":"hOP"},{"location":"common/util/#isarray","text":"Determines if a supplied variable represents an array. import { isArray } from \"@pnp/core\"; let x:String[] = [1,2,3]]; if (isArray(x)){ console.log(\"I am an array\"); }else{ console.log(\"I am not an array\"); }","title":"isArray"},{"location":"common/util/#isfunc","text":"Determines if a supplied variable represents a function. import { isFunc } from \"@pnp/core\"; public testFunction() { console.log(\"test function\"); return } if (isFunc(testFunction)){ console.log(\"this is a function\"); testFunction(); }","title":"isFunc"},{"location":"common/util/#isurlabsolute","text":"Determines if a supplied url is absolute and returns true; otherwise returns false. import { isUrlAbsolute } from \"@pnp/core\"; const webPath = 'https://{tenant}.sharepoint.com/sites/dev/'; if (isUrlAbsolute(webPath)){ console.log(\"URL is absolute\"); }else{ console.log(\"URL is not absolute\"); }","title":"isUrlAbsolute"},{"location":"common/util/#objectdefinednotnull","text":"Determines if an object is defined and not null. import { objectDefinedNotNull } from \"@pnp/core\"; let obj = { prop: 1 }; if (objectDefinedNotNull(obj)){ console.log(\"Not null\"); }else{ console.log(\"Null\"); }","title":"objectDefinedNotNull"},{"location":"common/util/#stringisnullorempty","text":"Determines if a supplied string is null or empty. import { stringIsNullOrEmpty } from \"@pnp/core\"; let x:String = \"hello\"; if (stringIsNullOrEmpty(x)){ console.log(\"Null or empty\"); }else{ console.log(\"Not null or empty\"); }","title":"stringIsNullOrEmpty"},{"location":"common/util/#removed","text":"Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods below for use in your projects should you require. /** * Loads a stylesheet into the current page * * @param path The url to the stylesheet * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues */ public static loadStylesheet(path: string, avoidCache: boolean): void { if (avoidCache) { path += \"?\" + encodeURIComponent((new Date()).getTime().toString()); } const head = document.getElementsByTagName(\"head\"); if (head.length > 0) { const e = document.createElement(\"link\"); head[0].appendChild(e); e.setAttribute(\"type\", \"text/css\"); e.setAttribute(\"rel\", \"stylesheet\"); e.setAttribute(\"href\", path); } } /** * Tests if a url param exists * * @param name The name of the url parameter to check */ public static urlParamExists(name: string): boolean { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); return regex.test(location.search); } /** * Gets a url param value by name * * @param name The name of the parameter for which we want the value */ public static getUrlParamByName(name: string): string { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); const results = regex.exec(location.search); return results == null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \")); } /** * Gets a url param by name and attempts to parse a bool value * * @param name The name of the parameter for which we want the boolean value */ public static getUrlParamBoolByName(name: string): boolean { const p = this.getUrlParamByName(name); const isFalse = (p === \"\" || /false|0/i.test(p)); return !isFalse; } /** * Inserts the string s into the string target as the index specified by index * * @param target The string into which we will insert s * @param index The location in target to insert s (zero based) * @param s The string to insert into target at position index */ public static stringInsert(target: string, index: number, s: string): string { if (index > 0) { return target.substring(0, index) + s + target.substring(index, target.length); } return s + target; }","title":"Removed"},{"location":"concepts/configuration/","text":"PnPjs Configuration \u00b6 This article describes the configuration architecture used by the library as well as the settings available. Starting with version 2.1.0 we updated our configuration design to support the ability to isolate settings to individual objects. The first part of this article discusses the newer design, you can read about the pre v2.1.0 configuration further down. Post v2.1.0 \u00b6 Architecture \u00b6 Starting from v2.1.0 we have modified our configuration design to allow for configuring individual queryable objects. Backward Compatibility \u00b6 If you have no need to use the isolated runtimes introduced in 2.1.0 then you should see no change in library behavior from prior versions. You can continue to refer to the pre v2.1.0 configuration section - and if you see any issues please let us know. All of the available settings as described below remain, unchanged. If you previously used our internal configuration classes directly RuntimeConfigImpl, SPRuntimeConfigImpl, or GraphRuntimeConfigImpl they no longer exist. We do not consider this a breaking change as they were meant to be internal and their direct use was not documented. This includes the concrete default instances RuntimeConfig, SPRuntimeConfig, and GraphRuntimeConfig. Isolated Runtimes \u00b6 You can create an isolated runtime when using either the sp or graph libraries. What this does is create an isolated set of properties and behaviors specific to a given fluent chain. Have a look at this basic example below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuration applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuration applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the web at https://mytenant.sharepoint.com/ const web1 = await sp.web(); // details for the web at https://mytenant.sharepoint.com/sites/dev const web2 = await isolatedSP.web(); This configuration is supplied to all objects down a given fluent chain: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuraiton applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuraiton applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the lists at https://mytenant.sharepoint.com/ const lists1 = await sp.web.lists(); // details for the lists at https://mytenant.sharepoint.com/sites/dev const lists2 = await isolatedSP.web.lists(); createIsolated \u00b6 The createIsolated method is used to establish the isolated runtime for a given instance of either the sp or graph libraries. Once created it is no longer connected to the default instance and if you have common settings that must be updated you would need to update them across each isolated instance, this is by design. Currently sp and graph createIsolated methods accept the same init, but we have broken them out to make thing clear. All properties of the init object are optional. Any properties provided will overwrite those cloned from the default if cloneGlobal is true. If cloneGlobal is false you start with an empty config containing only the core defaults . sp.createIsolated \u00b6 import { sp, ISPConfiguration } from \"@pnp/sp\"; // accept all the defaults, will clone any settings from sp const isolatedSP = await sp.createIsolated(); // - specify all the config options, using the ISPConfiguration interface to type the config // - setting baseUrl in the root is equivelent to setting it with sp: { baseUrl: }, it is provided as a shortcut as this seemed to be a common use case // - if you set them both the baseUrl in the root will be used. // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedSP = await sp.createIsolated({ baseUrl: \"https://mytenant.sharepoint.com\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, sp: { baseUrl: \"https://mytenant.sharepoint.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, }); Defaults Name Default baseUrl \"\" cloneGlobal true config {} options {} graph.createIsolated \u00b6 import { graph, IGraphConfiguration } from \"@pnp/graph\"; // - specify all the config options, using the IGraphConfiguration interface to type the config // - setting baseUrl in the root is restricted to \"v1.0\" or \"beta\". If you need to specify a different absolute url should use config.graph.baseUrl // - in practice you should use one or the other. You can always swap Graph api version using IGraphQueryable.setEndpoint // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedGraph = await graph.createIsolated({ baseUrl: \"v1.0\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, graph: { baseUrl: \"https://graph.microsoft.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, }); Defaults \u00b6 name Default baseUrl \"v1.0\" cloneGlobal true config {} options {} Additional Examples \u00b6 MSAL with Node multiple site requests \u00b6 MSAL Support Added in 2.0.11 In this example you can see how you can setup the MSAL client once and then set a different baseUrl for an isolated instance. More information specific to setting up the MSAL client is available . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev2/\", }, }, }); Node multiple site requests \u00b6 Isolated configuration was most requested for scenarios in node where you need to access information in multiple sites. This example shows setting up the global configuration and then creating an isolated config with only the baseUrl updated. import { SPFetchClient } from \"@pnp/nodejs\"; import { ISPConfigurationPart, sp } from \"@pnp/sp\"; sp.setup({ cacheExpirationIntervalMilliseconds: 1000, defaultCachingStore: \"local\", sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/\", \"id\", \"secret\"); }, headers: { \"X-MyRequiredHeader\": \"SomeValue\", \"X-MyRequiredHeader2\": \"SomeValue\", }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/site/dev\", \"id\", \"secret\"); }, }, }, }); Batching \u00b6 All batching functionality works as expected, but you must take care to only associate requests from the same isolated instance as you create the batch. Mixing requests across isolation boundaries is not supported. This applies to sp and graph batching. sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"url1\", \"id\", \"secret\"); }, }, }); const isolated = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"url2\", \"id\", \"secret\"); }, }, }, }); const batch1 = sp.createBatch(); sp.web.lists.select(\"Title\").top(3).inBatch(batch1)().then(r => console.log(`here 1: ${JSON.stringify(r, null, 2)}`)); sp.web.select(\"Title\").inBatch(batch1)().then(r => console.log(`here 2: ${JSON.stringify(r, null, 2)}`)); await batch1.execute(); const batch2 = isolated.createBatch(); isolated.web.lists.select(\"Title\").top(3).inBatch(batch2)().then(r => console.log(`here 3: ${JSON.stringify(r, null, 2)}`)); isolated.web.select(\"Title\").inBatch(batch2)().then(r => console.log(`here 4: ${JSON.stringify(r, null, 2)}`)); await batch2.execute(); IE11 Mode \u00b6 The IE11 mode setting is always global. There is no scenario we care to support where once instance needs to run in ie11 mode and another does not. Your code either does or does not run in ie11. Prior to v2.1.0 \u00b6 Architecture \u00b6 PnPjs uses an additive configuration design with multiple libraries sharing a single global configuration instance. If you need non-global configuration please see this section . There are three ways to access the setup functionality - through either the common, sp, or graph library's setup method. While the configuration is global the various methods have different typing on their input parameter. You can review the libconfig article for more details on storing your own configuration. Common Configuration \u00b6 The common libary's setup method takes parameters defined by ILibraryConfiguration . The properties and their defaults are listed below, followed by a code sample. You can call setup multiple times and any new values will be added to the existing configuration or replace the previous value if one existed. All values are optional. Name Description Default defaultCachingStore Where will PnPjs store cached data by default (session or local) session defaultCachingTimeoutSeconds The global default value used for cached data timeouts in seconds 60 globalCacheDisable Provides a way to globally within PnPjs disable all caching false enableCacheExpiration If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval false cacheExpirationIntervalMilliseconds Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) 750 spfxContext When running in SPFx the current context should always be supplied to PnPjs when available null ie11 If true the library downgrades functionality to work in IE11 false For more information on setting up in SPFx please see the authentication section For more details on ie11 mode please see the topic article import { setup } from \"@pnp/core\"; // called before other code setup({ cacheExpirationIntervalMilliseconds: 15000, defaultCachingStore: \"local\", defaultCachingTimeoutSeconds: 600, enableCacheExpiration: true, globalCacheDisable: false, ie11: false, spfxContext: this.context, // if in SPFx, otherwise leave it out }); SP Configuration \u00b6 The sp library's configuration is defined by the ISPConfiguration interface which extends ILibraryConfiguration. All of the sp values are contained in a top level property named \"sp\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { sp } from \"@pnp/sp\"; import { SPFxAdalClient } from \"@pnp/core\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration sp.setup({ ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", fetchClientFactory: () => { return new SPFxAdalClient(this.context); }, headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, }); SharePoint Framework \u00b6 You can optionally supply only the SPFx context to the sp configure method. import { sp } from \"@pnp/sp\"; // in SPFx only sp.setup(this.context); Graph Configuration \u00b6 The graph configuration works exactly the same as the sp configuration but is defined by the IGraphConfiguration interface which extends ILibraryConfiguration. All of the graph values are contained in a top level property named \"graph\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. ( Added in 2.0.8 ) none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { graph } from \"@pnp/graph\"; import { MsalClientSetup } from \"@pnp/msaljsclient\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration graph.setup({ ie11: false, graph: { // we set the GCC url baseUrl: \"https://graph.microsoft.us\", fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, }); SharePoint Framework \u00b6 You can optionally supply only the SPFx context to the graph configure method. We will attempt to set the baseUrl property from the context - but if that is failing in your environment and you need to call a special cloud (i.e. graph.microsoft.us) please set the baseUrl property. import { graph } from \"@pnp/graph\"; // in SPFx only graph.setup(this.context); Configure Everything At Once \u00b6 In some cases you might want to configure everything in one go. Because the configuration is stored in a single location you can use the common library's setup method and adjust the typings to ensure you are using the correct property names while only having to setup things with a single call. In versions before 2.0.8 ISPConfigurationPart, IGraphConfigurationPart, and ILibraryConfiguration incorrectly were missing the \"I\" prefix. That was fixed in 2.0.8 - but note if you are using an older version of the library you'll need to use the old names. Everything else in the below example works as expected. import { ISPConfigurationPart } from \"@pnp/sp\"; import { IGraphConfigurationPart } from \"@pnp/graph\"; import { ILibraryConfiguration, setup } from \"@pnp/core\"; // you could also include your custom configuration parts export interface AllConfig extends ILibraryConfiguration, ISPConfigurationPart, IGraphConfigurationPart { } // create a single big configuration entry const config: AllConfig = { graph: { baseUrl: \"https://graph.microsoft.us\", }, ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", }, }; setup(config);","title":"Configuration"},{"location":"concepts/configuration/#pnpjs-configuration","text":"This article describes the configuration architecture used by the library as well as the settings available. Starting with version 2.1.0 we updated our configuration design to support the ability to isolate settings to individual objects. The first part of this article discusses the newer design, you can read about the pre v2.1.0 configuration further down.","title":"PnPjs Configuration"},{"location":"concepts/configuration/#post-v210","text":"","title":"Post v2.1.0"},{"location":"concepts/configuration/#architecture","text":"Starting from v2.1.0 we have modified our configuration design to allow for configuring individual queryable objects.","title":"Architecture"},{"location":"concepts/configuration/#backward-compatibility","text":"If you have no need to use the isolated runtimes introduced in 2.1.0 then you should see no change in library behavior from prior versions. You can continue to refer to the pre v2.1.0 configuration section - and if you see any issues please let us know. All of the available settings as described below remain, unchanged. If you previously used our internal configuration classes directly RuntimeConfigImpl, SPRuntimeConfigImpl, or GraphRuntimeConfigImpl they no longer exist. We do not consider this a breaking change as they were meant to be internal and their direct use was not documented. This includes the concrete default instances RuntimeConfig, SPRuntimeConfig, and GraphRuntimeConfig.","title":"Backward Compatibility"},{"location":"concepts/configuration/#isolated-runtimes","text":"You can create an isolated runtime when using either the sp or graph libraries. What this does is create an isolated set of properties and behaviors specific to a given fluent chain. Have a look at this basic example below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuration applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuration applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the web at https://mytenant.sharepoint.com/ const web1 = await sp.web(); // details for the web at https://mytenant.sharepoint.com/sites/dev const web2 = await isolatedSP.web(); This configuration is supplied to all objects down a given fluent chain: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuraiton applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuraiton applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the lists at https://mytenant.sharepoint.com/ const lists1 = await sp.web.lists(); // details for the lists at https://mytenant.sharepoint.com/sites/dev const lists2 = await isolatedSP.web.lists();","title":"Isolated Runtimes"},{"location":"concepts/configuration/#createisolated","text":"The createIsolated method is used to establish the isolated runtime for a given instance of either the sp or graph libraries. Once created it is no longer connected to the default instance and if you have common settings that must be updated you would need to update them across each isolated instance, this is by design. Currently sp and graph createIsolated methods accept the same init, but we have broken them out to make thing clear. All properties of the init object are optional. Any properties provided will overwrite those cloned from the default if cloneGlobal is true. If cloneGlobal is false you start with an empty config containing only the core defaults .","title":"createIsolated"},{"location":"concepts/configuration/#spcreateisolated","text":"import { sp, ISPConfiguration } from \"@pnp/sp\"; // accept all the defaults, will clone any settings from sp const isolatedSP = await sp.createIsolated(); // - specify all the config options, using the ISPConfiguration interface to type the config // - setting baseUrl in the root is equivelent to setting it with sp: { baseUrl: }, it is provided as a shortcut as this seemed to be a common use case // - if you set them both the baseUrl in the root will be used. // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedSP = await sp.createIsolated({ baseUrl: \"https://mytenant.sharepoint.com\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, sp: { baseUrl: \"https://mytenant.sharepoint.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, }); Defaults Name Default baseUrl \"\" cloneGlobal true config {} options {}","title":"sp.createIsolated"},{"location":"concepts/configuration/#graphcreateisolated","text":"import { graph, IGraphConfiguration } from \"@pnp/graph\"; // - specify all the config options, using the IGraphConfiguration interface to type the config // - setting baseUrl in the root is restricted to \"v1.0\" or \"beta\". If you need to specify a different absolute url should use config.graph.baseUrl // - in practice you should use one or the other. You can always swap Graph api version using IGraphQueryable.setEndpoint // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedGraph = await graph.createIsolated({ baseUrl: \"v1.0\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, graph: { baseUrl: \"https://graph.microsoft.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, });","title":"graph.createIsolated"},{"location":"concepts/configuration/#defaults","text":"name Default baseUrl \"v1.0\" cloneGlobal true config {} options {}","title":"Defaults"},{"location":"concepts/configuration/#additional-examples","text":"","title":"Additional Examples"},{"location":"concepts/configuration/#msal-with-node-multiple-site-requests","text":"MSAL Support Added in 2.0.11 In this example you can see how you can setup the MSAL client once and then set a different baseUrl for an isolated instance. More information specific to setting up the MSAL client is available . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev2/\", }, }, });","title":"MSAL with Node multiple site requests"},{"location":"concepts/configuration/#node-multiple-site-requests","text":"Isolated configuration was most requested for scenarios in node where you need to access information in multiple sites. This example shows setting up the global configuration and then creating an isolated config with only the baseUrl updated. import { SPFetchClient } from \"@pnp/nodejs\"; import { ISPConfigurationPart, sp } from \"@pnp/sp\"; sp.setup({ cacheExpirationIntervalMilliseconds: 1000, defaultCachingStore: \"local\", sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/\", \"id\", \"secret\"); }, headers: { \"X-MyRequiredHeader\": \"SomeValue\", \"X-MyRequiredHeader2\": \"SomeValue\", }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/site/dev\", \"id\", \"secret\"); }, }, }, });","title":"Node multiple site requests"},{"location":"concepts/configuration/#batching","text":"All batching functionality works as expected, but you must take care to only associate requests from the same isolated instance as you create the batch. Mixing requests across isolation boundaries is not supported. This applies to sp and graph batching. sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"url1\", \"id\", \"secret\"); }, }, }); const isolated = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"url2\", \"id\", \"secret\"); }, }, }, }); const batch1 = sp.createBatch(); sp.web.lists.select(\"Title\").top(3).inBatch(batch1)().then(r => console.log(`here 1: ${JSON.stringify(r, null, 2)}`)); sp.web.select(\"Title\").inBatch(batch1)().then(r => console.log(`here 2: ${JSON.stringify(r, null, 2)}`)); await batch1.execute(); const batch2 = isolated.createBatch(); isolated.web.lists.select(\"Title\").top(3).inBatch(batch2)().then(r => console.log(`here 3: ${JSON.stringify(r, null, 2)}`)); isolated.web.select(\"Title\").inBatch(batch2)().then(r => console.log(`here 4: ${JSON.stringify(r, null, 2)}`)); await batch2.execute();","title":"Batching"},{"location":"concepts/configuration/#ie11-mode","text":"The IE11 mode setting is always global. There is no scenario we care to support where once instance needs to run in ie11 mode and another does not. Your code either does or does not run in ie11.","title":"IE11 Mode"},{"location":"concepts/configuration/#prior-to-v210","text":"","title":"Prior to v2.1.0"},{"location":"concepts/configuration/#architecture_1","text":"PnPjs uses an additive configuration design with multiple libraries sharing a single global configuration instance. If you need non-global configuration please see this section . There are three ways to access the setup functionality - through either the common, sp, or graph library's setup method. While the configuration is global the various methods have different typing on their input parameter. You can review the libconfig article for more details on storing your own configuration.","title":"Architecture"},{"location":"concepts/configuration/#common-configuration","text":"The common libary's setup method takes parameters defined by ILibraryConfiguration . The properties and their defaults are listed below, followed by a code sample. You can call setup multiple times and any new values will be added to the existing configuration or replace the previous value if one existed. All values are optional. Name Description Default defaultCachingStore Where will PnPjs store cached data by default (session or local) session defaultCachingTimeoutSeconds The global default value used for cached data timeouts in seconds 60 globalCacheDisable Provides a way to globally within PnPjs disable all caching false enableCacheExpiration If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval false cacheExpirationIntervalMilliseconds Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) 750 spfxContext When running in SPFx the current context should always be supplied to PnPjs when available null ie11 If true the library downgrades functionality to work in IE11 false For more information on setting up in SPFx please see the authentication section For more details on ie11 mode please see the topic article import { setup } from \"@pnp/core\"; // called before other code setup({ cacheExpirationIntervalMilliseconds: 15000, defaultCachingStore: \"local\", defaultCachingTimeoutSeconds: 600, enableCacheExpiration: true, globalCacheDisable: false, ie11: false, spfxContext: this.context, // if in SPFx, otherwise leave it out });","title":"Common Configuration"},{"location":"concepts/configuration/#sp-configuration","text":"The sp library's configuration is defined by the ISPConfiguration interface which extends ILibraryConfiguration. All of the sp values are contained in a top level property named \"sp\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { sp } from \"@pnp/sp\"; import { SPFxAdalClient } from \"@pnp/core\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration sp.setup({ ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", fetchClientFactory: () => { return new SPFxAdalClient(this.context); }, headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, });","title":"SP Configuration"},{"location":"concepts/configuration/#sharepoint-framework","text":"You can optionally supply only the SPFx context to the sp configure method. import { sp } from \"@pnp/sp\"; // in SPFx only sp.setup(this.context);","title":"SharePoint Framework"},{"location":"concepts/configuration/#graph-configuration","text":"The graph configuration works exactly the same as the sp configuration but is defined by the IGraphConfiguration interface which extends ILibraryConfiguration. All of the graph values are contained in a top level property named \"graph\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. ( Added in 2.0.8 ) none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { graph } from \"@pnp/graph\"; import { MsalClientSetup } from \"@pnp/msaljsclient\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration graph.setup({ ie11: false, graph: { // we set the GCC url baseUrl: \"https://graph.microsoft.us\", fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, });","title":"Graph Configuration"},{"location":"concepts/configuration/#sharepoint-framework_1","text":"You can optionally supply only the SPFx context to the graph configure method. We will attempt to set the baseUrl property from the context - but if that is failing in your environment and you need to call a special cloud (i.e. graph.microsoft.us) please set the baseUrl property. import { graph } from \"@pnp/graph\"; // in SPFx only graph.setup(this.context);","title":"SharePoint Framework"},{"location":"concepts/configuration/#configure-everything-at-once","text":"In some cases you might want to configure everything in one go. Because the configuration is stored in a single location you can use the common library's setup method and adjust the typings to ensure you are using the correct property names while only having to setup things with a single call. In versions before 2.0.8 ISPConfigurationPart, IGraphConfigurationPart, and ILibraryConfiguration incorrectly were missing the \"I\" prefix. That was fixed in 2.0.8 - but note if you are using an older version of the library you'll need to use the old names. Everything else in the below example works as expected. import { ISPConfigurationPart } from \"@pnp/sp\"; import { IGraphConfigurationPart } from \"@pnp/graph\"; import { ILibraryConfiguration, setup } from \"@pnp/core\"; // you could also include your custom configuration parts export interface AllConfig extends ILibraryConfiguration, ISPConfigurationPart, IGraphConfigurationPart { } // create a single big configuration entry const config: AllConfig = { graph: { baseUrl: \"https://graph.microsoft.us\", }, ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", }, }; setup(config);","title":"Configure Everything At Once"},{"location":"concepts/custom-bundle/","text":"Custom Bundling \u00b6 With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles. Scenarios could include: Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once. Creating SPFx libraries either for one project or a single webpart. Create a single library containing the PnPjs code you need bundled along with your custom extensions . Create a custom bundle \u00b6 Webpack \u00b6 You can see/clone a sample project of this example here . Rollup \u00b6 You can see/clone a sample project of this example here .","title":"Custom Bundle"},{"location":"concepts/custom-bundle/#custom-bundling","text":"With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles. Scenarios could include: Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once. Creating SPFx libraries either for one project or a single webpart. Create a single library containing the PnPjs code you need bundled along with your custom extensions .","title":"Custom Bundling"},{"location":"concepts/custom-bundle/#create-a-custom-bundle","text":"","title":"Create a custom bundle"},{"location":"concepts/custom-bundle/#webpack","text":"You can see/clone a sample project of this example here .","title":"Webpack"},{"location":"concepts/custom-bundle/#rollup","text":"You can see/clone a sample project of this example here .","title":"Rollup"},{"location":"concepts/error-handling/","text":"Error Handling \u00b6 This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns. For 429, 503, and 504 errors we include retry logic within the library The HttpRequestError \u00b6 All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error . In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible. Property Name Description name Standard Error.name property. Always 'Error' message Normalized string containing the status, status text, and the full response text stack The callstack producing the error isHttpRequestError Always true, allows you to reliably determine if you have an HttpRequestError instance response Unread copy of the Response object resulting in the thrown error status The Response.status value (such as 404) statusText The Response.statusText value (such as 'Not Found') Basic Handling \u00b6 For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen \ud83d\ude09. The most basic type of error handling involves a simple try-catch. import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { console.error(e); } This will produce output like: Error making HttpClient request in queryable [404] Not Found ::> {\"odata.error\":{\"code\":\"-1, System.ArgumentException\",\"message\":{\"lang\":\"en-US\",\"value\":\"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'.\"}}} Data: {\"response\":{\"size\":0,\"timeout\":0},\"status\":404,\"statusText\":\"Not Found\",\"isHttpRequestError\":true} This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly. Reading the Response \u00b6 In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire: import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { // are we dealing with an HttpRequestError? if (e?.isHttpRequestError) { // we can read the json from the response const json = await (e).response.json(); // if we have a value property we can show it console.log(typeof json[\"odata.error\"] === \"object\" ? json[\"odata.error\"].message.value : e.message); // add of course you have access to the other properties and can make choices on how to act if ((e).status === 404) { console.error((e).statusText); // maybe create the resource, or redirect, or fallback to a secondary data source // just ideas, handle any of the status codes uniquely as needed } } else { // not an HttpRequestError so we just log message console.log(e.message); } } Logging errors \u00b6 Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework. import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { Logger.error(e); } You may want to read the response and customize the message as described above: import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { if (e?.isHttpRequestError) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } } Putting it All Together \u00b6 After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application. errorhandler.ts \u00b6 import { Logger } from \"@pnp/logging\"; import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { if (hOP(e, \"isHttpRequestError\")) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } } web-request.ts \u00b6 import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { await handleError(e); } web-request2.ts \u00b6 import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists(); } catch (e) { await handleError(e); }","title":"Error Handling"},{"location":"concepts/error-handling/#error-handling","text":"This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns. For 429, 503, and 504 errors we include retry logic within the library","title":"Error Handling"},{"location":"concepts/error-handling/#the-httprequesterror","text":"All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error . In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible. Property Name Description name Standard Error.name property. Always 'Error' message Normalized string containing the status, status text, and the full response text stack The callstack producing the error isHttpRequestError Always true, allows you to reliably determine if you have an HttpRequestError instance response Unread copy of the Response object resulting in the thrown error status The Response.status value (such as 404) statusText The Response.statusText value (such as 'Not Found')","title":"The HttpRequestError"},{"location":"concepts/error-handling/#basic-handling","text":"For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen \ud83d\ude09. The most basic type of error handling involves a simple try-catch. import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { console.error(e); } This will produce output like: Error making HttpClient request in queryable [404] Not Found ::> {\"odata.error\":{\"code\":\"-1, System.ArgumentException\",\"message\":{\"lang\":\"en-US\",\"value\":\"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'.\"}}} Data: {\"response\":{\"size\":0,\"timeout\":0},\"status\":404,\"statusText\":\"Not Found\",\"isHttpRequestError\":true} This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.","title":"Basic Handling"},{"location":"concepts/error-handling/#reading-the-response","text":"In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire: import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { // are we dealing with an HttpRequestError? if (e?.isHttpRequestError) { // we can read the json from the response const json = await (e).response.json(); // if we have a value property we can show it console.log(typeof json[\"odata.error\"] === \"object\" ? json[\"odata.error\"].message.value : e.message); // add of course you have access to the other properties and can make choices on how to act if ((e).status === 404) { console.error((e).statusText); // maybe create the resource, or redirect, or fallback to a secondary data source // just ideas, handle any of the status codes uniquely as needed } } else { // not an HttpRequestError so we just log message console.log(e.message); } }","title":"Reading the Response"},{"location":"concepts/error-handling/#logging-errors","text":"Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework. import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { Logger.error(e); } You may want to read the response and customize the message as described above: import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { if (e?.isHttpRequestError) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } }","title":"Logging errors"},{"location":"concepts/error-handling/#putting-it-all-together","text":"After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.","title":"Putting it All Together"},{"location":"concepts/error-handling/#errorhandlerts","text":"import { Logger } from \"@pnp/logging\"; import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { if (hOP(e, \"isHttpRequestError\")) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } }","title":"errorhandler.ts"},{"location":"concepts/error-handling/#web-requestts","text":"import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { await handleError(e); }","title":"web-request.ts"},{"location":"concepts/error-handling/#web-request2ts","text":"import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists(); } catch (e) { await handleError(e); }","title":"web-request2.ts"},{"location":"concepts/ie11-mode/","text":"IE11 Mode \u00b6 Starting with v2 we have made the decision to no longer support IE11. Because we know this affects folks we have introduced IE11 compatibility mode. Configuring the library will allow it to work within IE11, however at a possibly reduced level of functionality depending on your use case. Please see the list below of known limitations. Limitations \u00b6 When required to use IE11 mode there is certain functionality which may not work correctly or at all. Unavailable: Extension Methods Unavailable: OData Debugging Configure IE11 Mode \u00b6 To enable IE11 Mode set the ie11 flag to true in the setup object. Optionally, supply the context object when working in SharePoint Framework . import { sp } from \"@pnp/sp\"; sp.setup({ // set ie 11 mode ie11: true, // only needed when working within SharePoint Framework spfxContext: this.context }); If you are supporting IE 11, please see the article on required polyfills . A note on ie11 mode and support \u00b6 Because IE11 is no longer a primary supported browser our policy moving forward will be doing our best not to break anything in ie11 mode, but not all features will work and new features may never come to ie11 mode. Also, if you find an ie11 bug we expect you to work with us on helping to fix it. If you aren't willing to invest some time to support an old browser it seems we shouldn't either.","title":"IE11 Mode"},{"location":"concepts/ie11-mode/#ie11-mode","text":"Starting with v2 we have made the decision to no longer support IE11. Because we know this affects folks we have introduced IE11 compatibility mode. Configuring the library will allow it to work within IE11, however at a possibly reduced level of functionality depending on your use case. Please see the list below of known limitations.","title":"IE11 Mode"},{"location":"concepts/ie11-mode/#limitations","text":"When required to use IE11 mode there is certain functionality which may not work correctly or at all. Unavailable: Extension Methods Unavailable: OData Debugging","title":"Limitations"},{"location":"concepts/ie11-mode/#configure-ie11-mode","text":"To enable IE11 Mode set the ie11 flag to true in the setup object. Optionally, supply the context object when working in SharePoint Framework . import { sp } from \"@pnp/sp\"; sp.setup({ // set ie 11 mode ie11: true, // only needed when working within SharePoint Framework spfxContext: this.context }); If you are supporting IE 11, please see the article on required polyfills .","title":"Configure IE11 Mode"},{"location":"concepts/ie11-mode/#a-note-on-ie11-mode-and-support","text":"Because IE11 is no longer a primary supported browser our policy moving forward will be doing our best not to break anything in ie11 mode, but not all features will work and new features may never come to ie11 mode. Also, if you find an ie11 bug we expect you to work with us on helping to fix it. If you aren't willing to invest some time to support an old browser it seems we shouldn't either.","title":"A note on ie11 mode and support"},{"location":"concepts/invokable/","text":"Invokables \u00b6 For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain: // an example of get const lists = await sp.web.lists(); Starting with v2 this is no longer required, you can invoke the object directly to execute the default action for that class - typically a get. const lists = await sp.web.lists(); This has two main benefits for people using the library: you can write less code, and we now have a way to model default actions for objects that might do something other than a get. The way we designed the library prior to v2 hid the post, put, delete operations as protected methods attached to the Queryable classes. Without diving into why we did this, having a rethink seemed appropriate for v2. Based on that, the entire queryable chain is now invokable as well for any of the operations. Other Operations (post, put, delete) \u00b6 import { sp, spPost } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // do a post to a web - just an example doesn't do anything fancy spPost(sp.web); Things get a little more interesting in that you can now do posts (or any of the operations) to any of the urls defined by a fluent chain. Meaning you can easily implement methods that are not yet part of the library. For this example I have made up a method called \"MagicFieldCreationMethod\" that doesn't exist. Imagine it was just added to the SharePoint API and we do not yet have support for it. You can now write code like so: import { sp, spPost, SharePointQueryable } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields/web\"; // call our made up example method spPost(SharePointQueryable(sp.web.fields, \"MagicFieldCreationMethod\"), { body: JSON.stringify({ // ... this would be the post body }), });","title":"Invokables"},{"location":"concepts/invokable/#invokables","text":"For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain: // an example of get const lists = await sp.web.lists(); Starting with v2 this is no longer required, you can invoke the object directly to execute the default action for that class - typically a get. const lists = await sp.web.lists(); This has two main benefits for people using the library: you can write less code, and we now have a way to model default actions for objects that might do something other than a get. The way we designed the library prior to v2 hid the post, put, delete operations as protected methods attached to the Queryable classes. Without diving into why we did this, having a rethink seemed appropriate for v2. Based on that, the entire queryable chain is now invokable as well for any of the operations.","title":"Invokables"},{"location":"concepts/invokable/#other-operations-post-put-delete","text":"import { sp, spPost } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // do a post to a web - just an example doesn't do anything fancy spPost(sp.web); Things get a little more interesting in that you can now do posts (or any of the operations) to any of the urls defined by a fluent chain. Meaning you can easily implement methods that are not yet part of the library. For this example I have made up a method called \"MagicFieldCreationMethod\" that doesn't exist. Imagine it was just added to the SharePoint API and we do not yet have support for it. You can now write code like so: import { sp, spPost, SharePointQueryable } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields/web\"; // call our made up example method spPost(SharePointQueryable(sp.web.fields, \"MagicFieldCreationMethod\"), { body: JSON.stringify({ // ... this would be the post body }), });","title":"Other Operations (post, put, delete)"},{"location":"concepts/polyfill/","text":"Polyfills \u00b6 These libraries may make use of some features not found in older browsers. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. If you are supporting IE11 enable IE11 mode . IE 11 Polyfill package \u00b6 We created a package you try and help provide this missing functionality. This package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you are required to support IE 11. Install \u00b6 npm install @pnp/polyfill-ie11 --save Use \u00b6 import \"@pnp/polyfill-ie11\"; import { sp } from \"@pnp/sp/presets/all\"; sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); }); Selective Use \u00b6 Starting with version 2.0.2 you can selectively include the polyfills from the package. Depending on your needs it may make sense in your application to use the underlying libraries directly. We have added an expanded statement on our polyfills . // individually include polyfills as needed to match your requirements import \"@pnp/polyfill-ie11/dist/fetch\"; import \"@pnp/polyfill-ie11/dist/fill\"; import \"@pnp/polyfill-ie11/dist/from\"; import \"@pnp/polyfill-ie11/dist/iterator\"; import \"@pnp/polyfill-ie11/dist/map\"; import \"@pnp/polyfill-ie11/dist/promise\"; import \"@pnp/polyfill-ie11/dist/reflect\"; import \"@pnp/polyfill-ie11/dist/symbol\"; // works in IE11 and other browsers sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); }); SearchQueryBuilder \u00b6 Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version as shown below. import \"@pnp/polyfill-ie11\"; import { SearchQueryBuilder } from \"@pnp/polyfill-ie11/dist/searchquerybuilder\"; import { sp, ISearchQueryBuilder } from \"@pnp/sp/presets/all\"; // works in IE11 and other browsers const builder: ISearchQueryBuilder = SearchQueryBuilder().text(\"test\"); sp.search(builder).then(r => { this.domElement.innerHTML = JSON.stringify(r); }); General Statement on Polyfills \u00b6 Internet Explorer 11 (IE11) has been an enterprise standard browser for many years. Given the complexity in changing technical platforms in many organizations, it is no surprise standardization on this out-of-date browser continues. Unfortunately, for those organizations, the Internet has moved on and many - if not all - SaaS platforms are embracing modern standards and no longer supporting the legacy IE11 browser. Even Microsoft states in their official documentation that Microsoft 365 is best experienced with a modern browser. They have even gone so far to build the latest version of Microsoft Edge based on Chromium (Edge Chromium), with an \"Internet Explorer mode\" allowing organizations to load legacy sites which require IE automatically. PnPjs is now \"modern\" as well, and by that we mean we have moved to using capabilities of current browsers and JavaScript which are not present in IE11. We understand as a developer your ability to require an organization to switch browsers is unrealistic. We want to do everything we can to support you, but it is up to you to ensure your application is properly supported in IE11. There are many polyfills available, depending on the platform you're running on, the frameworks you are using, and the libraries you consume. Although the majority of PnPjs users build for SharePoint Online, a significant number build for earlier versions of the platform as well as for their own node-based solutions or websites. Unfortunately, there is no way our polyfill library can support all these scenarios. What we intended with the @pnp/polyfill-ie11 package was to provide a comprehensive group of all the polyfills that would be needed based on the complete PnPjs library. We are finding when we aggregate our polyfills with the polyfills provided in the SharePoint page and from other sources, things don't always work well. We cannot solve this for your specific situations except by providing you transparency into the polyfills which we know are necessary for our packages. You may need to adjust what polyfills your application uses based on the other libraries you are using. To that end, we want to provide the list of polyfills we recommend here - along with the associated packages \u2013 with the goal of helping you to work out what combination of polyfills might work with your code. Also, if you haven't reviewed it yet, please check out the information on IE11 Mode for how to configure IE11 mode in the sp.setup as well as what limitations doing so will have on your usage of PnPjs. imports import \"core-js/stable/array/from\"; import \"core-js/stable/array/fill\"; import \"core-js/stable/array/iterator\"; import \"core-js/stable/promise\"; import \"core-js/stable/reflect\"; import \"es6-map/implement\"; import \"core-js/stable/symbol\"; import \"whatwg-fetch\"; The following NPM packages are what we use to do the above indicated imports |package| |---| | core-js | | es6-map | | whatwg-fetch |","title":"Polyfills"},{"location":"concepts/polyfill/#polyfills","text":"These libraries may make use of some features not found in older browsers. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. If you are supporting IE11 enable IE11 mode .","title":"Polyfills"},{"location":"concepts/polyfill/#ie-11-polyfill-package","text":"We created a package you try and help provide this missing functionality. This package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you are required to support IE 11.","title":"IE 11 Polyfill package"},{"location":"concepts/polyfill/#install","text":"npm install @pnp/polyfill-ie11 --save","title":"Install"},{"location":"concepts/polyfill/#use","text":"import \"@pnp/polyfill-ie11\"; import { sp } from \"@pnp/sp/presets/all\"; sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); });","title":"Use"},{"location":"concepts/polyfill/#selective-use","text":"Starting with version 2.0.2 you can selectively include the polyfills from the package. Depending on your needs it may make sense in your application to use the underlying libraries directly. We have added an expanded statement on our polyfills . // individually include polyfills as needed to match your requirements import \"@pnp/polyfill-ie11/dist/fetch\"; import \"@pnp/polyfill-ie11/dist/fill\"; import \"@pnp/polyfill-ie11/dist/from\"; import \"@pnp/polyfill-ie11/dist/iterator\"; import \"@pnp/polyfill-ie11/dist/map\"; import \"@pnp/polyfill-ie11/dist/promise\"; import \"@pnp/polyfill-ie11/dist/reflect\"; import \"@pnp/polyfill-ie11/dist/symbol\"; // works in IE11 and other browsers sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); });","title":"Selective Use"},{"location":"concepts/polyfill/#searchquerybuilder","text":"Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version as shown below. import \"@pnp/polyfill-ie11\"; import { SearchQueryBuilder } from \"@pnp/polyfill-ie11/dist/searchquerybuilder\"; import { sp, ISearchQueryBuilder } from \"@pnp/sp/presets/all\"; // works in IE11 and other browsers const builder: ISearchQueryBuilder = SearchQueryBuilder().text(\"test\"); sp.search(builder).then(r => { this.domElement.innerHTML = JSON.stringify(r); });","title":"SearchQueryBuilder"},{"location":"concepts/polyfill/#general-statement-on-polyfills","text":"Internet Explorer 11 (IE11) has been an enterprise standard browser for many years. Given the complexity in changing technical platforms in many organizations, it is no surprise standardization on this out-of-date browser continues. Unfortunately, for those organizations, the Internet has moved on and many - if not all - SaaS platforms are embracing modern standards and no longer supporting the legacy IE11 browser. Even Microsoft states in their official documentation that Microsoft 365 is best experienced with a modern browser. They have even gone so far to build the latest version of Microsoft Edge based on Chromium (Edge Chromium), with an \"Internet Explorer mode\" allowing organizations to load legacy sites which require IE automatically. PnPjs is now \"modern\" as well, and by that we mean we have moved to using capabilities of current browsers and JavaScript which are not present in IE11. We understand as a developer your ability to require an organization to switch browsers is unrealistic. We want to do everything we can to support you, but it is up to you to ensure your application is properly supported in IE11. There are many polyfills available, depending on the platform you're running on, the frameworks you are using, and the libraries you consume. Although the majority of PnPjs users build for SharePoint Online, a significant number build for earlier versions of the platform as well as for their own node-based solutions or websites. Unfortunately, there is no way our polyfill library can support all these scenarios. What we intended with the @pnp/polyfill-ie11 package was to provide a comprehensive group of all the polyfills that would be needed based on the complete PnPjs library. We are finding when we aggregate our polyfills with the polyfills provided in the SharePoint page and from other sources, things don't always work well. We cannot solve this for your specific situations except by providing you transparency into the polyfills which we know are necessary for our packages. You may need to adjust what polyfills your application uses based on the other libraries you are using. To that end, we want to provide the list of polyfills we recommend here - along with the associated packages \u2013 with the goal of helping you to work out what combination of polyfills might work with your code. Also, if you haven't reviewed it yet, please check out the information on IE11 Mode for how to configure IE11 mode in the sp.setup as well as what limitations doing so will have on your usage of PnPjs. imports import \"core-js/stable/array/from\"; import \"core-js/stable/array/fill\"; import \"core-js/stable/array/iterator\"; import \"core-js/stable/promise\"; import \"core-js/stable/reflect\"; import \"es6-map/implement\"; import \"core-js/stable/symbol\"; import \"whatwg-fetch\"; The following NPM packages are what we use to do the above indicated imports |package| |---| | core-js | | es6-map | | whatwg-fetch |","title":"General Statement on Polyfills"},{"location":"concepts/selective-imports/","text":"Selective Imports \u00b6 As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports in v2. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking . This concept works well with custom bundling to create a shared package tailored exactly to your needs. If you would prefer to not worry about selective imports please see the section on presets . Old way \u00b6 // the sp var came with all library functionality already attached // meaning treeshaking couldn't reduce the size import { sp } from \"@pnp/sp\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); New Way \u00b6 // the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific // the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example: // this import statement will attach content-type functionality to list, web, and item import \"@pnp/sp/content-types\"; // this import statement will only attach content-type functionality to web import \"@pnp/sp/content-types/web\"; If you only need to access content types on the web object you can reduce size by only importing that piece. // this will fail import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IList } from \"@pnp/sp/lists\"; // do this instead import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { IList } from \"@pnp/sp/lists\"; const lists = await sp.web.lists(); Presets \u00b6 Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually. SP \u00b6 For the sp library there are two presets \"all\" and \"core\". The all preset mimics the behavior in v1 and includes everything in the library already attached to the sp var. import { sp } from \"@pnp/sp/presets/all\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists(); The \"core\" preset includes sites, webs, lists, and items. import { sp } from \"@pnp/sp/presets/core\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists(); Graph \u00b6 The graph library contains a single preset, \"all\" mimicking the v1 structure. import { graph } from \"@pnp/graph/presets/all\"; // graph.* exists as it did in v1, tree shaking will not work While we may look to add additional presets in the future you are encouraged to look at making your own custom bundles as a preferred solution.","title":"Selective Imports"},{"location":"concepts/selective-imports/#selective-imports","text":"As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports in v2. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking . This concept works well with custom bundling to create a shared package tailored exactly to your needs. If you would prefer to not worry about selective imports please see the section on presets .","title":"Selective Imports"},{"location":"concepts/selective-imports/#old-way","text":"// the sp var came with all library functionality already attached // meaning treeshaking couldn't reduce the size import { sp } from \"@pnp/sp\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();","title":"Old way"},{"location":"concepts/selective-imports/#new-way","text":"// the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific // the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example: // this import statement will attach content-type functionality to list, web, and item import \"@pnp/sp/content-types\"; // this import statement will only attach content-type functionality to web import \"@pnp/sp/content-types/web\"; If you only need to access content types on the web object you can reduce size by only importing that piece. // this will fail import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IList } from \"@pnp/sp/lists\"; // do this instead import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { IList } from \"@pnp/sp/lists\"; const lists = await sp.web.lists();","title":"New Way"},{"location":"concepts/selective-imports/#presets","text":"Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually.","title":"Presets"},{"location":"concepts/selective-imports/#sp","text":"For the sp library there are two presets \"all\" and \"core\". The all preset mimics the behavior in v1 and includes everything in the library already attached to the sp var. import { sp } from \"@pnp/sp/presets/all\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists(); The \"core\" preset includes sites, webs, lists, and items. import { sp } from \"@pnp/sp/presets/core\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists();","title":"SP"},{"location":"concepts/selective-imports/#graph","text":"The graph library contains a single preset, \"all\" mimicking the v1 structure. import { graph } from \"@pnp/graph/presets/all\"; // graph.* exists as it did in v1, tree shaking will not work While we may look to add additional presets in the future you are encouraged to look at making your own custom bundles as a preferred solution.","title":"Graph"},{"location":"concepts/settings/","text":"Project Settings \u00b6 This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally. The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root . Settings File Format (>= 2.0.13) \u00b6 Starting with version 2.0.13 we have added support within the settings file for MSAL authentication for both SharePoint and Graph. You are NOT required to update your existing settings file unless you want to use MSAL authentication with a Graph application. The existing id/secret settings continue to work however we recommend updating when you have an opportunity. For more information coinfiguring MSAL please review the section in the authentication section for node . MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always \"https://{tenant}.sharepoint.com/.default\" or \"https://graph.microsoft.com/.default\" depending on what you are calling. If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated. const privateKey = `-----BEGIN RSA PRIVATE KEY----- your private key, read from a file or included here -----END RSA PRIVATE KEY----- `; var msalInit = { auth: { authority: \"https://login.microsoftonline.com/{tenant id}\", clientCertificate: { thumbprint: \"{certificate thumbnail}\", privateKey: privateKey, }, clientId: \"{AAD App registration id}\", } } var settings = { testing: { enableWebTests: true, testUser: \"i:0#.f|membership|user@consto.com\", sp: { url: \"{required for MSAL - absolute url of test site}\", notificationUrl: \"{ optional: notification url }\", msal: { init: msalInit, scopes: [\"https://{tenant}.sharepoint.com/.default\"] }, }, graph: { msal: { init: msalInit, scopes: [\"https://graph.microsoft.com/.default\"] }, }, }, } module.exports = settings; The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below. enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. testUser AAD login account to be used when running tests. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests SP values \u00b6 name description url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions msal Information about MSAL authentication setup Graph value \u00b6 The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description msal Information about MSAL authentication setup Settings File Format (<= 2.0.12) \u00b6 var settings = { testing: { enableWebTests: true, sp: { id: \"{ client id }\", secret: \"{ client secret }\", url: \"{ site collection url }\", notificationUrl: \"{ optional: notification url }\", }, graph: { tenant: \"{tenant.onmicrosoft.com}\", id: \"{your app id}\", secret: \"{your secret}\" }, } } module.exports = settings; enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests SP values \u00b6 The sp values are described in the table below and come from registering a legacy SharePoint add-in . name description id The client id of the registered application secret The client secret of the registered application url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions Graph values \u00b6 The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description tenant Tenant to target for authentication and data (ex: contoso.onmicrosoft.com) id The application id secret The application secret Create Settings.js file \u00b6 Copy the example file and rename it settings.js. Place the file in the root of your project. Update the settings as needed for your environment. If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.","title":"Settings"},{"location":"concepts/settings/#project-settings","text":"This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally. The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root .","title":"Project Settings"},{"location":"concepts/settings/#settings-file-format-2013","text":"Starting with version 2.0.13 we have added support within the settings file for MSAL authentication for both SharePoint and Graph. You are NOT required to update your existing settings file unless you want to use MSAL authentication with a Graph application. The existing id/secret settings continue to work however we recommend updating when you have an opportunity. For more information coinfiguring MSAL please review the section in the authentication section for node . MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always \"https://{tenant}.sharepoint.com/.default\" or \"https://graph.microsoft.com/.default\" depending on what you are calling. If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated. const privateKey = `-----BEGIN RSA PRIVATE KEY----- your private key, read from a file or included here -----END RSA PRIVATE KEY----- `; var msalInit = { auth: { authority: \"https://login.microsoftonline.com/{tenant id}\", clientCertificate: { thumbprint: \"{certificate thumbnail}\", privateKey: privateKey, }, clientId: \"{AAD App registration id}\", } } var settings = { testing: { enableWebTests: true, testUser: \"i:0#.f|membership|user@consto.com\", sp: { url: \"{required for MSAL - absolute url of test site}\", notificationUrl: \"{ optional: notification url }\", msal: { init: msalInit, scopes: [\"https://{tenant}.sharepoint.com/.default\"] }, }, graph: { msal: { init: msalInit, scopes: [\"https://graph.microsoft.com/.default\"] }, }, }, } module.exports = settings; The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below. enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. testUser AAD login account to be used when running tests. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests","title":"Settings File Format (>= 2.0.13)"},{"location":"concepts/settings/#sp-values","text":"name description url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions msal Information about MSAL authentication setup","title":"SP values"},{"location":"concepts/settings/#graph-value","text":"The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description msal Information about MSAL authentication setup","title":"Graph value"},{"location":"concepts/settings/#settings-file-format-2012","text":"var settings = { testing: { enableWebTests: true, sp: { id: \"{ client id }\", secret: \"{ client secret }\", url: \"{ site collection url }\", notificationUrl: \"{ optional: notification url }\", }, graph: { tenant: \"{tenant.onmicrosoft.com}\", id: \"{your app id}\", secret: \"{your secret}\" }, } } module.exports = settings; enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests","title":"Settings File Format (<= 2.0.12)"},{"location":"concepts/settings/#sp-values_1","text":"The sp values are described in the table below and come from registering a legacy SharePoint add-in . name description id The client id of the registered application secret The client secret of the registered application url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions","title":"SP values"},{"location":"concepts/settings/#graph-values","text":"The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description tenant Tenant to target for authentication and data (ex: contoso.onmicrosoft.com) id The application id secret The application secret","title":"Graph values"},{"location":"concepts/settings/#create-settingsjs-file","text":"Copy the example file and rename it settings.js. Place the file in the root of your project. Update the settings as needed for your environment. If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.","title":"Create Settings.js file"},{"location":"config-store/","text":"@pnp/config-store \u00b6 This module provides a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/sp @pnp/config-store --save See the topics below for usage: configuration providers","title":"config-store"},{"location":"config-store/#pnpconfig-store","text":"This module provides a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed.","title":"@pnp/config-store"},{"location":"config-store/#getting-started","text":"Install the library and required dependencies npm install @pnp/sp @pnp/config-store --save See the topics below for usage: configuration providers","title":"Getting Started"},{"location":"config-store/configuration/","text":"@pnp/config-store/configuration \u00b6 The main class exported from the config-store package is Settings. This is the class through which you will load and access your settings via providers . import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create an instance of the settings class, could be static and shared across your application // or built as needed. const settings = new Settings(); // you can add/update a single value using add settings.add(\"mykey\", \"myvalue\"); // you can also add/update a JSON value which will be stringified for you as a shorthand settings.addJSON(\"mykey2\", { field: 1, field2: 2, field3: 3, }); // and you can apply a plain object of keys/values that will be written as single values // this results in each enumerable property of the supplied object being added to the settings collection settings.apply({ field: 1, field2: 2, field3: 3, }); // and finally you can load values from a configuration provider const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // this will load values from the supplied list // by default the key will be from the Title field and the value from a column named Value await settings.load(provider); // once we have loaded values we can then read them const value = settings.get(\"mykey\"); // or read JSON that will be parsed for you from the store const value2 = settings.getJSON(\"mykey2\");","title":"configuration"},{"location":"config-store/configuration/#pnpconfig-storeconfiguration","text":"The main class exported from the config-store package is Settings. This is the class through which you will load and access your settings via providers . import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create an instance of the settings class, could be static and shared across your application // or built as needed. const settings = new Settings(); // you can add/update a single value using add settings.add(\"mykey\", \"myvalue\"); // you can also add/update a JSON value which will be stringified for you as a shorthand settings.addJSON(\"mykey2\", { field: 1, field2: 2, field3: 3, }); // and you can apply a plain object of keys/values that will be written as single values // this results in each enumerable property of the supplied object being added to the settings collection settings.apply({ field: 1, field2: 2, field3: 3, }); // and finally you can load values from a configuration provider const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // this will load values from the supplied list // by default the key will be from the Title field and the value from a column named Value await settings.load(provider); // once we have loaded values we can then read them const value = settings.get(\"mykey\"); // or read JSON that will be parsed for you from the store const value2 = settings.getJSON(\"mykey2\");","title":"@pnp/config-store/configuration"},{"location":"config-store/providers/","text":"@pnp/config-store/providers \u00b6 Currently there is a single provider included in the library, but contributions of additional providers are welcome. SPListConfigurationProvider \u00b6 This provider is based on a SharePoint list it reads all of the rows and makes them available as a TypedHash . By default the column names used are Title for key and \"Value\" for value, but you can update these as needed. Additionally, the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); const settings = new Settings(); // load our values from the list await settings.load(provider); CachingConfigurationProvider \u00b6 Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a provider and caches the configuration in local or session storage. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // get an instance of the provider wrapped // you can optionally provide a key that will be used in the cache to the asCaching method const wrappedProvider = provider.asCaching(); // use that wrapped provider to populate the settings await settings.load(wrappedProvider);","title":"providers"},{"location":"config-store/providers/#pnpconfig-storeproviders","text":"Currently there is a single provider included in the library, but contributions of additional providers are welcome.","title":"@pnp/config-store/providers"},{"location":"config-store/providers/#splistconfigurationprovider","text":"This provider is based on a SharePoint list it reads all of the rows and makes them available as a TypedHash . By default the column names used are Title for key and \"Value\" for value, but you can update these as needed. Additionally, the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); const settings = new Settings(); // load our values from the list await settings.load(provider);","title":"SPListConfigurationProvider"},{"location":"config-store/providers/#cachingconfigurationprovider","text":"Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a provider and caches the configuration in local or session storage. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // get an instance of the provider wrapped // you can optionally provide a key that will be used in the cache to the asCaching method const wrappedProvider = provider.asCaching(); // use that wrapped provider to populate the settings await settings.load(wrappedProvider);","title":"CachingConfigurationProvider"},{"location":"contributing/","text":"Contributing to PnPjs \u00b6 Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality. Section Description Setup Dev Machine Covers setting up your machine to ensure you are ready to debug the solution Local Debug Configuration Discusses the steps required to establish local configuration used for debugging and running tests Debugging Describes how to debug PnPjs locally Extending the library Basic examples on how to extend the library such as adding a method or property Writing Tests How to write and debug tests Update Documentation Describes the steps required to edit and locally view the documentation Submit a Pull Request Outlines guidance for submitting a pull request Need Help? \u00b6 The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website.","title":"Contributing"},{"location":"contributing/#contributing-to-pnpjs","text":"Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality. Section Description Setup Dev Machine Covers setting up your machine to ensure you are ready to debug the solution Local Debug Configuration Discusses the steps required to establish local configuration used for debugging and running tests Debugging Describes how to debug PnPjs locally Extending the library Basic examples on how to extend the library such as adding a method or property Writing Tests How to write and debug tests Update Documentation Describes the steps required to edit and locally view the documentation Submit a Pull Request Outlines guidance for submitting a pull request","title":"Contributing to PnPjs"},{"location":"contributing/#need-help","text":"The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website.","title":"Need Help?"},{"location":"contributing/debug-tests/","text":"Writing Tests \u00b6 With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place. How to write Tests \u00b6 We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts : import { getRandomString } from \"@pnp/core\"; import { testSettings } from \"../main\"; import { expect } from \"chai\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import { IList } from \"@pnp/sp/lists\"; describe(\"Items\", () => { // any tests that make a web request should be withing a block checking if web tests are enabled if (testSettings.enableWebTests) { // a block scoped var we will use across our tests let list: IList = null; // we use the before block to setup // executed before all the tests in this block, see the mocha docs for more details // mocha prefers using function vs arrow functions and this is recommended before(async function () { // execute a request to ensure we have a list const ler = await sp.web.lists.ensure(\"ItemTestList\", \"Used to test item operations\"); list = ler.list; // in this case we want to have some items in the list for testing so we add those // only if the list was just created if (ler.created) { // add a few items to get started const batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); await batch.execute(); } }); // this test has a label \"get items\" and is run via an async function it(\"get items\", async function () { // make a request for the list's items const items = await list.items(); // report that we expect that result to be an array with more than 0 items expect(items.length).to.be.gt(0); }); // ... remainder of code removed } } General Guidelines for Writing Tests \u00b6 Tests should operate within the site defined in testSettings Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll When writing tests you can use \"only\" and \"skip\" from mochajs to focus on only the tests you are writing Be sure to review the various options when running your tests If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description Next Steps \u00b6 Now that you've written tests to cover your changes you'll need to update the docs .","title":"Writing Tests"},{"location":"contributing/debug-tests/#writing-tests","text":"With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.","title":"Writing Tests"},{"location":"contributing/debug-tests/#how-to-write-tests","text":"We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts : import { getRandomString } from \"@pnp/core\"; import { testSettings } from \"../main\"; import { expect } from \"chai\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import { IList } from \"@pnp/sp/lists\"; describe(\"Items\", () => { // any tests that make a web request should be withing a block checking if web tests are enabled if (testSettings.enableWebTests) { // a block scoped var we will use across our tests let list: IList = null; // we use the before block to setup // executed before all the tests in this block, see the mocha docs for more details // mocha prefers using function vs arrow functions and this is recommended before(async function () { // execute a request to ensure we have a list const ler = await sp.web.lists.ensure(\"ItemTestList\", \"Used to test item operations\"); list = ler.list; // in this case we want to have some items in the list for testing so we add those // only if the list was just created if (ler.created) { // add a few items to get started const batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); await batch.execute(); } }); // this test has a label \"get items\" and is run via an async function it(\"get items\", async function () { // make a request for the list's items const items = await list.items(); // report that we expect that result to be an array with more than 0 items expect(items.length).to.be.gt(0); }); // ... remainder of code removed } }","title":"How to write Tests"},{"location":"contributing/debug-tests/#general-guidelines-for-writing-tests","text":"Tests should operate within the site defined in testSettings Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll When writing tests you can use \"only\" and \"skip\" from mochajs to focus on only the tests you are writing Be sure to review the various options when running your tests If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description","title":"General Guidelines for Writing Tests"},{"location":"contributing/debug-tests/#next-steps","text":"Now that you've written tests to cover your changes you'll need to update the docs .","title":"Next Steps"},{"location":"contributing/debugging/","text":"Debugging \u00b6 Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on. Before proceeding be sure you have reviewed how to setup for local configuration and debugging. Debugging Library Features \u00b6 The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point. Basic SharePoint Testing \u00b6 You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules . All of the setup for the node client is handled within sp.ts using the settings from the local configuration . Basic Graph Testing \u00b6 Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit. All of the setup for the node client is handled within graph.ts using the settings from the local configuration . How to: Create a Debug Module \u00b6 If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git. Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content: // note we can use the actual package names for our imports (ex: @pnp/logging) import { Logger, LogLevel, ConsoleListener } from \"@pnp/logging\"; // using the all preset for simplicity in the example, selective imports work as expected import { sp, ListEnsureResult } from \"@pnp/sp/presets/all\"; declare var process: { exit(code?: number): void }; export async function MyDebug() { // configure your options // you can have different configs in different modules as needed for your testing/dev work sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret); }, }, }); // run some debugging const list = await sp.web.lists.ensure(\"MyFirstList\"); Logger.log({ data: list.created, level: LogLevel.Info, message: \"Was list created?\", }); if (list.created) { Logger.log({ data: list.data, level: LogLevel.Info, message: \"Raw data from list creation.\", }); } else { Logger.log({ data: null, level: LogLevel.Info, message: \"List already existed!\", }); } process.exit(0); } Update main.ts to launch your module \u00b6 First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this: // ... // comment out the example // import { Example } from \"./example\"; // Example(); import { MyDebug } from \"./mydebug\" MyDebug(); // ... Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file) Debug \u00b6 Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios. Debug Module Next Steps \u00b6 Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run. In Browser Debugging \u00b6 You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js , allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner. Start the local serve \u00b6 This will serve a package with ./debug/serve/main.ts as the entry. gulp serve Add reference to library \u00b6 Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
    You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files. Debug \u00b6 Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it. Next Steps \u00b6 You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser. Now you can learn about extending the library .","title":"Debugging"},{"location":"contributing/debugging/#debugging","text":"Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on. Before proceeding be sure you have reviewed how to setup for local configuration and debugging.","title":"Debugging"},{"location":"contributing/debugging/#debugging-library-features","text":"The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.","title":"Debugging Library Features"},{"location":"contributing/debugging/#basic-sharepoint-testing","text":"You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules . All of the setup for the node client is handled within sp.ts using the settings from the local configuration .","title":"Basic SharePoint Testing"},{"location":"contributing/debugging/#basic-graph-testing","text":"Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit. All of the setup for the node client is handled within graph.ts using the settings from the local configuration .","title":"Basic Graph Testing"},{"location":"contributing/debugging/#how-to-create-a-debug-module","text":"If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git. Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content: // note we can use the actual package names for our imports (ex: @pnp/logging) import { Logger, LogLevel, ConsoleListener } from \"@pnp/logging\"; // using the all preset for simplicity in the example, selective imports work as expected import { sp, ListEnsureResult } from \"@pnp/sp/presets/all\"; declare var process: { exit(code?: number): void }; export async function MyDebug() { // configure your options // you can have different configs in different modules as needed for your testing/dev work sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret); }, }, }); // run some debugging const list = await sp.web.lists.ensure(\"MyFirstList\"); Logger.log({ data: list.created, level: LogLevel.Info, message: \"Was list created?\", }); if (list.created) { Logger.log({ data: list.data, level: LogLevel.Info, message: \"Raw data from list creation.\", }); } else { Logger.log({ data: null, level: LogLevel.Info, message: \"List already existed!\", }); } process.exit(0); }","title":"How to: Create a Debug Module"},{"location":"contributing/debugging/#update-maints-to-launch-your-module","text":"First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this: // ... // comment out the example // import { Example } from \"./example\"; // Example(); import { MyDebug } from \"./mydebug\" MyDebug(); // ... Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)","title":"Update main.ts to launch your module"},{"location":"contributing/debugging/#debug","text":"Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.","title":"Debug"},{"location":"contributing/debugging/#debug-module-next-steps","text":"Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.","title":"Debug Module Next Steps"},{"location":"contributing/debugging/#in-browser-debugging","text":"You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js , allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.","title":"In Browser Debugging"},{"location":"contributing/debugging/#start-the-local-serve","text":"This will serve a package with ./debug/serve/main.ts as the entry. gulp serve","title":"Start the local serve"},{"location":"contributing/debugging/#add-reference-to-library","text":"Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
    You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.","title":"Add reference to library"},{"location":"contributing/debugging/#debug_1","text":"Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.","title":"Debug"},{"location":"contributing/debugging/#next-steps","text":"You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser. Now you can learn about extending the library .","title":"Next Steps"},{"location":"contributing/documentation/","text":"Documentation \u00b6 Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request. Writing Docs \u00b6 Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources. Building Docs Locally \u00b6 Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable . When executing the pip module on Windows you can prefix it with python -m . For example: python -m pip install mkdocs-material Install MkDocs pip install mkdocs Install the Material theme pip install mkdocs-material install the mkdocs-markdownextradata-plugin - this is used for the version variable pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) install redirect plugin - used to redirect from moved pages pip install mkdocs-redirects Serve it up mkdocs serve Open a browser to http://127.0.0.1:8000/ Please see the official mkdocs site for more details on working with mkdocs Next Steps \u00b6 After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request !","title":"Update Documentation"},{"location":"contributing/documentation/#documentation","text":"Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.","title":"Documentation"},{"location":"contributing/documentation/#writing-docs","text":"Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.","title":"Writing Docs"},{"location":"contributing/documentation/#building-docs-locally","text":"Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable . When executing the pip module on Windows you can prefix it with python -m . For example: python -m pip install mkdocs-material Install MkDocs pip install mkdocs Install the Material theme pip install mkdocs-material install the mkdocs-markdownextradata-plugin - this is used for the version variable pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) install redirect plugin - used to redirect from moved pages pip install mkdocs-redirects Serve it up mkdocs serve Open a browser to http://127.0.0.1:8000/ Please see the official mkdocs site for more details on working with mkdocs","title":"Building Docs Locally"},{"location":"contributing/documentation/#next-steps","text":"After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request !","title":"Next Steps"},{"location":"contributing/extending-the-library/","text":"Extending PnPjs \u00b6 This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property. At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the \"webs\" property is added to the web class. // TypeScript property, returning an interface public get webs(): IWebs { // using the Webs factory function and providing \"this\" as the first parameter return Webs(this); } Understanding Factory Functions \u00b6 PnPjs v2 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form. // create a constant which is a function of type ISPInvokableFactory having the name Webs // this is bound by the generic type param to return an IWebs instance // and it will use the _Webs concrete class to form the internal type of the invocable export const Webs = spInvokableFactory(_Webs); The ISPInvokableFactory type looks like: export type ISPInvokableFactory = (baseUrl: string | ISharePointQueryable, path?: string) => R; And the matching graph type: (f: any): (baseUrl: string | IGraphQueryable, path?: string) => R The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples. import { Web } from \"@pnp/sp/webs\"; // create a web from an absolute url const web = Web(\"https://tenant.sharepoint.com\"); // as an example, create a new web using the first as a base // targets: https://tenant.sharepoint.com/sites/dev const web2 = Web(web, \"sites/dev\"); // or you can add any path components you want, here as an example we access the current user property const cu = Web(web, \"currentuser\"); const currentUserInfo = cu(); Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their \"type\". It is an important concept when working with the library to always remember we are just building strings. Class structure \u00b6 Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method /* The concrete class implementation. This is never exported or shown directly to consumers of the library. It is wrapped by the Proxy we do expose. It extends the _SharePointQueryableInstance class for which there is a matching _SharePointQueryableCollection. The generic parameter defines the return type of a get operation and the invoked result. Classes can have methods and properties as normal. This one has a single property as a simple example */ export class _HubSite extends _SharePointQueryableInstance { /** * Gets the ISite instance associated with this hub site */ // the tag decorator is used to provide some additional telemetry on what methods are // being called. @tag(\"hs.getSite\") public async getSite(): Promise { // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result const d = await this.select(\"SiteUrl\")(); // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl return Site(d.SiteUrl); } } /* This defines the interface we export and expose to consumers. In most cases this extends the concrete object but may add or remove some methods/properties in special cases */ export interface IHubSite extends _HubSite { } /* This defines the HubSite factory function as discussed above binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite. This is understood to mean that HubSite is a factory function that returns a types of IHubSite which the spInvokableFactory will create using _HubSite as the concrete underlying type. */ export const HubSite = spInvokableFactory(_HubSite); Add a Property \u00b6 In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class. export class _View extends _SharePointQueryableInstance { // ... other code removed // add the property, and provide a return type // return types should be interfaces public get fields(): IViewFields { // we use the ViewFields factory function supplying \"this\" as the first parameter // this will create a url like \".../fields/viewfields\" due to the defaultPath decorator // on the _ViewFields class. This is equivalent to: ViewFields(this, \"viewfields\") return ViewFields(this); } // ... other code removed } There are many examples throughout the library that follow this pattern. Add a Method \u00b6 Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method: @defaultPath(\"items\") export class _Items extends _SharePointQueryableCollection { /** * Gets an Item by id * * @param id The integer id of the item to retrieve */ // we declare a method and set the return type to an interface public getById(id: number): IItem { // here we use the tag helper to add some telemetry to our request // we create a new IItem using the factory and appending the id value to the end // this gives us a valid url path to a single item .../items/getById(2) // we can then use the returned IItem to extend our chain or execute a request return tag.configure(Item(this).concat(`(${id})`), \"is.getById\"); } // ... other code removed } Web Request Method \u00b6 A second example is a method that performs a request. Here we use the _Item recycle method as an example: /** * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item. */ // we use the tag decorator to add telemetry @tag(\"i.recycle\") // we return a promise public recycle(): Promise { // we use the spPost method to post the request created by cloning our current instance IItem using // the Item factory and adding the path \"recycle\" to the end. Url will look like .../items/getById(2)/recycle return spPost(this.clone(Item, \"recycle\")); } Augment Using Selective Imports \u00b6 To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available. // import the addProp helper import { addProp } from \"@pnp/queryable\"; // import the _List concrete class from the types module (not the index!) import { _List } from \"../lists/types\"; // import the interface and factory we are going to add to the List import { Items, IItems } from \"./types\"; // This module declaration fixes up the types, allowing .items to appear in intellisense // when you import \"@pnp/sp/items/list\"; declare module \"../lists/types\" { // we need to extend the concrete type interface _List { readonly items: IItems; } // we need to extend the interface // this may not be strictly necessary as the IList interface extends _List so it // should pick up the same additions, but we have seen in some cases this does seem // to be required. So we include it for safety as it will all be removed during // transpilation we don't need to care about the extra code interface IList { readonly items: IItems; } } // finally we add the property to the _List class // this method call says add a property to _List named \"items\" and that property returns a result using the Items factory // The factory will be called with \"this\" when the property is accessed. If needed there is a fourth parameter to append additional path // information to the property url addProp(_List, \"items\", Items); General Rules for Extending PnPjs \u00b6 Only expose interfaces to consumers Use the factory functions except in very special cases Look for other properties and methods as examples Simple is always preferable, but not always possible - use your best judgement If you find yourself writing a ton of code to solve a problem you think should be easy, ask If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed Next Steps \u00b6 Now that you have extended the library you need to write a test to cover it!","title":"Extending the library"},{"location":"contributing/extending-the-library/#extending-pnpjs","text":"This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property. At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the \"webs\" property is added to the web class. // TypeScript property, returning an interface public get webs(): IWebs { // using the Webs factory function and providing \"this\" as the first parameter return Webs(this); }","title":"Extending PnPjs"},{"location":"contributing/extending-the-library/#understanding-factory-functions","text":"PnPjs v2 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form. // create a constant which is a function of type ISPInvokableFactory having the name Webs // this is bound by the generic type param to return an IWebs instance // and it will use the _Webs concrete class to form the internal type of the invocable export const Webs = spInvokableFactory(_Webs); The ISPInvokableFactory type looks like: export type ISPInvokableFactory = (baseUrl: string | ISharePointQueryable, path?: string) => R; And the matching graph type: (f: any): (baseUrl: string | IGraphQueryable, path?: string) => R The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples. import { Web } from \"@pnp/sp/webs\"; // create a web from an absolute url const web = Web(\"https://tenant.sharepoint.com\"); // as an example, create a new web using the first as a base // targets: https://tenant.sharepoint.com/sites/dev const web2 = Web(web, \"sites/dev\"); // or you can add any path components you want, here as an example we access the current user property const cu = Web(web, \"currentuser\"); const currentUserInfo = cu(); Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their \"type\". It is an important concept when working with the library to always remember we are just building strings.","title":"Understanding Factory Functions"},{"location":"contributing/extending-the-library/#class-structure","text":"Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method /* The concrete class implementation. This is never exported or shown directly to consumers of the library. It is wrapped by the Proxy we do expose. It extends the _SharePointQueryableInstance class for which there is a matching _SharePointQueryableCollection. The generic parameter defines the return type of a get operation and the invoked result. Classes can have methods and properties as normal. This one has a single property as a simple example */ export class _HubSite extends _SharePointQueryableInstance { /** * Gets the ISite instance associated with this hub site */ // the tag decorator is used to provide some additional telemetry on what methods are // being called. @tag(\"hs.getSite\") public async getSite(): Promise { // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result const d = await this.select(\"SiteUrl\")(); // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl return Site(d.SiteUrl); } } /* This defines the interface we export and expose to consumers. In most cases this extends the concrete object but may add or remove some methods/properties in special cases */ export interface IHubSite extends _HubSite { } /* This defines the HubSite factory function as discussed above binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite. This is understood to mean that HubSite is a factory function that returns a types of IHubSite which the spInvokableFactory will create using _HubSite as the concrete underlying type. */ export const HubSite = spInvokableFactory(_HubSite);","title":"Class structure"},{"location":"contributing/extending-the-library/#add-a-property","text":"In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class. export class _View extends _SharePointQueryableInstance { // ... other code removed // add the property, and provide a return type // return types should be interfaces public get fields(): IViewFields { // we use the ViewFields factory function supplying \"this\" as the first parameter // this will create a url like \".../fields/viewfields\" due to the defaultPath decorator // on the _ViewFields class. This is equivalent to: ViewFields(this, \"viewfields\") return ViewFields(this); } // ... other code removed } There are many examples throughout the library that follow this pattern.","title":"Add a Property"},{"location":"contributing/extending-the-library/#add-a-method","text":"Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method: @defaultPath(\"items\") export class _Items extends _SharePointQueryableCollection { /** * Gets an Item by id * * @param id The integer id of the item to retrieve */ // we declare a method and set the return type to an interface public getById(id: number): IItem { // here we use the tag helper to add some telemetry to our request // we create a new IItem using the factory and appending the id value to the end // this gives us a valid url path to a single item .../items/getById(2) // we can then use the returned IItem to extend our chain or execute a request return tag.configure(Item(this).concat(`(${id})`), \"is.getById\"); } // ... other code removed }","title":"Add a Method"},{"location":"contributing/extending-the-library/#web-request-method","text":"A second example is a method that performs a request. Here we use the _Item recycle method as an example: /** * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item. */ // we use the tag decorator to add telemetry @tag(\"i.recycle\") // we return a promise public recycle(): Promise { // we use the spPost method to post the request created by cloning our current instance IItem using // the Item factory and adding the path \"recycle\" to the end. Url will look like .../items/getById(2)/recycle return spPost(this.clone(Item, \"recycle\")); }","title":"Web Request Method"},{"location":"contributing/extending-the-library/#augment-using-selective-imports","text":"To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available. // import the addProp helper import { addProp } from \"@pnp/queryable\"; // import the _List concrete class from the types module (not the index!) import { _List } from \"../lists/types\"; // import the interface and factory we are going to add to the List import { Items, IItems } from \"./types\"; // This module declaration fixes up the types, allowing .items to appear in intellisense // when you import \"@pnp/sp/items/list\"; declare module \"../lists/types\" { // we need to extend the concrete type interface _List { readonly items: IItems; } // we need to extend the interface // this may not be strictly necessary as the IList interface extends _List so it // should pick up the same additions, but we have seen in some cases this does seem // to be required. So we include it for safety as it will all be removed during // transpilation we don't need to care about the extra code interface IList { readonly items: IItems; } } // finally we add the property to the _List class // this method call says add a property to _List named \"items\" and that property returns a result using the Items factory // The factory will be called with \"this\" when the property is accessed. If needed there is a fourth parameter to append additional path // information to the property url addProp(_List, \"items\", Items);","title":"Augment Using Selective Imports"},{"location":"contributing/extending-the-library/#general-rules-for-extending-pnpjs","text":"Only expose interfaces to consumers Use the factory functions except in very special cases Look for other properties and methods as examples Simple is always preferable, but not always possible - use your best judgement If you find yourself writing a ton of code to solve a problem you think should be easy, ask If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed","title":"General Rules for Extending PnPjs"},{"location":"contributing/extending-the-library/#next-steps","text":"Now that you have extended the library you need to write a test to cover it!","title":"Next Steps"},{"location":"contributing/local-debug-configuration/","text":"Local Debugging Configuration \u00b6 This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly). Create settings.js \u00b6 Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. For more information the settings file please see Settings Minimal Configuration \u00b6 You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag. The following configuration file allows you to run all the tests that do not contact services. var sets = { testing: { enableWebTests: false, } } module.exports = sets; Test your setup \u00b6 If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.","title":"Local Debug Configuration"},{"location":"contributing/local-debug-configuration/#local-debugging-configuration","text":"This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).","title":"Local Debugging Configuration"},{"location":"contributing/local-debug-configuration/#create-settingsjs","text":"Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. For more information the settings file please see Settings","title":"Create settings.js"},{"location":"contributing/local-debug-configuration/#minimal-configuration","text":"You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag. The following configuration file allows you to run all the tests that do not contact services. var sets = { testing: { enableWebTests: false, } } module.exports = sets;","title":"Minimal Configuration"},{"location":"contributing/local-debug-configuration/#test-your-setup","text":"If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.","title":"Test your setup"},{"location":"contributing/pull-requests/","text":"Submitting Pull Requests \u00b6 Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release. Target your pull requests to the version-2 branch Add/Update any relevant docs articles in the relevant package's docs folder related to your changes Include a test for any new functionality and ensure all existing tests are passing by running npm test Ensure linting checks pass by typing npm run lint Ensure everything works for a build by running npm run package Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :) If you need to target a PR for version 1, please target the \"version-1\" branch Sharing is Caring - Pull Request Guidance \u00b6 The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website. Next Steps \u00b6 Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted. Thank you for helping PnPjs grow and improve!!","title":"Submit a Pull Request"},{"location":"contributing/pull-requests/#submitting-pull-requests","text":"Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release. Target your pull requests to the version-2 branch Add/Update any relevant docs articles in the relevant package's docs folder related to your changes Include a test for any new functionality and ensure all existing tests are passing by running npm test Ensure linting checks pass by typing npm run lint Ensure everything works for a build by running npm run package Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :) If you need to target a PR for version 1, please target the \"version-1\" branch","title":"Submitting Pull Requests"},{"location":"contributing/pull-requests/#sharing-is-caring-pull-request-guidance","text":"The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website.","title":"Sharing is Caring - Pull Request Guidance"},{"location":"contributing/pull-requests/#next-steps","text":"Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted. Thank you for helping PnPjs grow and improve!!","title":"Next Steps"},{"location":"contributing/setup-dev-machine/","text":"Setting up your Developer Machine \u00b6 If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging . Setup your development environment \u00b6 These steps will help you get your environment setup for contributing to the core library. Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like. Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). This library requires node >= 10.18.0 On Windows: Install Python [Optional] Install the tslint extension in VS Code: Press Shift + Ctrl + \"p\" to open the command panel Begin typing \"install extension\" and select the command when it appears in view Begin typing \"tslint\" and select the package when it appears in view Restart Code after installation Fork The Repo \u00b6 All of our contributions come via pull requests and you'll need to fork the repository Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool. Once you have the code locally, navigate to the root of the project in your console. Type the following command: npm install Follow the guidance to complete the one-time local configuration required to debug and run tests. Then you can follow the guidance in the debugging article.","title":"Setup Dev Machine"},{"location":"contributing/setup-dev-machine/#setting-up-your-developer-machine","text":"If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging .","title":"Setting up your Developer Machine"},{"location":"contributing/setup-dev-machine/#setup-your-development-environment","text":"These steps will help you get your environment setup for contributing to the core library. Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like. Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). This library requires node >= 10.18.0 On Windows: Install Python [Optional] Install the tslint extension in VS Code: Press Shift + Ctrl + \"p\" to open the command panel Begin typing \"install extension\" and select the command when it appears in view Begin typing \"tslint\" and select the package when it appears in view Restart Code after installation","title":"Setup your development environment"},{"location":"contributing/setup-dev-machine/#fork-the-repo","text":"All of our contributions come via pull requests and you'll need to fork the repository Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool. Once you have the code locally, navigate to the root of the project in your console. Type the following command: npm install Follow the guidance to complete the one-time local configuration required to debug and run tests. Then you can follow the guidance in the debugging article.","title":"Fork The Repo"},{"location":"graph/","text":"@pnp/graph \u00b6 This package contains the fluent api used to call the graph rest services. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application and access the root sp object import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; (function main() { // here we will load the current web's properties graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); }); })() Getting Started with SharePoint Framework \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application, update OnInit, and access the root sp object in render import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; // here we will load the current web's properties graph.groups().then(groups => { this.domElement.innerHTML = `Groups:
      ${groups.map(g => `
    • ${g.displayName}
    • `).join(\"\")}
    `; }); } Getting Started on Nodejs \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { graph } from \"@pnp/graph\"; import { AdalFetchClient } from \"@pnp/nodejs\"; import \"@pnp/graph/groups\"; // do this once per page load graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}.onmicrosoft.com\", \"AAD Application Id\", \"AAD Application Secret\"); }, }, }); // here we will load the groups information graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); });","title":"graph"},{"location":"graph/#pnpgraph","text":"This package contains the fluent api used to call the graph rest services.","title":"@pnp/graph"},{"location":"graph/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application and access the root sp object import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; (function main() { // here we will load the current web's properties graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); }); })()","title":"Getting Started"},{"location":"graph/#getting-started-with-sharepoint-framework","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application, update OnInit, and access the root sp object in render import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; // here we will load the current web's properties graph.groups().then(groups => { this.domElement.innerHTML = `Groups:
      ${groups.map(g => `
    • ${g.displayName}
    • `).join(\"\")}
    `; }); }","title":"Getting Started with SharePoint Framework"},{"location":"graph/#getting-started-on-nodejs","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { graph } from \"@pnp/graph\"; import { AdalFetchClient } from \"@pnp/nodejs\"; import \"@pnp/graph/groups\"; // do this once per page load graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}.onmicrosoft.com\", \"AAD Application Id\", \"AAD Application Secret\"); }, }, }); // here we will load the groups information graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); });","title":"Getting Started on Nodejs"},{"location":"graph/calendars/","text":"@pnp/graph/calendars \u00b6 Calendars exist in Outlook and can belong to either a user or group. With @pnp/graph@<=2.0.6 , only events for a user and group's default calendar could be fetched/created/updated. In versions 2.0.7 and up, all calendars and their events can be fetched. More information can be found in the official Graph documentation: Calendar Resource Type Event Resource Type ICalendar, ICalendars \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/calendars\"; Preset: All import { graph } from \"@pnp/graph/presets/all\"; Get All Calendars For a User \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars(); const myCalendars = await graph.me.calendars(); Get a Specific Calendar For a User \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)(); const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)(); Get a User's Default Calendar \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar(); const myCalendar = await graph.me.calendar(); Get Events For a User's Default Calendar \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // You can get the default calendar events const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events(); // or get all events for the user const events = await graph.users.getById('user@tenant.onmicrosoft.com').events(); // You can get my default calendar events const events = await graph.me.calendar.events(); // or get all events for me const events = await graph.me.events(); Get Events By ID \u00b6 You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA=='; const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; // Get events by ID const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID); const events = await graph.me.events.getByID(EventID); // Get an event by ID from a specific calendar const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID); const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID); Create Events \u00b6 This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add( { \"subject\": \"Let's go for lunch\", \"body\": { \"contentType\": \"HTML\", \"content\": \"Does late morning work for you?\" }, \"start\": { \"dateTime\": \"2017-04-15T12:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"end\": { \"dateTime\": \"2017-04-15T14:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"location\":{ \"displayName\":\"Harry's Bar\" }, \"attendees\": [ { \"emailAddress\": { \"address\":\"samanthab@contoso.onmicrosoft.com\", \"name\": \"Samantha Booth\" }, \"type\": \"required\" } ] }); Update Events \u00b6 This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({ reminderMinutesBeforeStart: 99, }); Delete Event \u00b6 This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete(); await graph.me.events.getById(EVENT_ID).delete(); Get Calendar for a Group \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar(); Get Events for a Group \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; // You can do one of const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events(); // or const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events(); Get Calendar View \u00b6 Added in 2.0.7 Gets the events in a calendar during a specified date range. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // basic request, note need to invoke the returned queryable const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\")(); // you can use select, top, etc to filter your returned results const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)(); // you can specify times along with the dates const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")(); const view4 = await graph.me.calendarView(\"2020-01-01\", \"2020-03-01\")();","title":"calendars"},{"location":"graph/calendars/#pnpgraphcalendars","text":"Calendars exist in Outlook and can belong to either a user or group. With @pnp/graph@<=2.0.6 , only events for a user and group's default calendar could be fetched/created/updated. In versions 2.0.7 and up, all calendars and their events can be fetched. More information can be found in the official Graph documentation: Calendar Resource Type Event Resource Type","title":"@pnp/graph/calendars"},{"location":"graph/calendars/#icalendar-icalendars","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/calendars\"; Preset: All import { graph } from \"@pnp/graph/presets/all\";","title":"ICalendar, ICalendars"},{"location":"graph/calendars/#get-all-calendars-for-a-user","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars(); const myCalendars = await graph.me.calendars();","title":"Get All Calendars For a User"},{"location":"graph/calendars/#get-a-specific-calendar-for-a-user","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)(); const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();","title":"Get a Specific Calendar For a User"},{"location":"graph/calendars/#get-a-users-default-calendar","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar(); const myCalendar = await graph.me.calendar();","title":"Get a User's Default Calendar"},{"location":"graph/calendars/#get-events-for-a-users-default-calendar","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // You can get the default calendar events const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events(); // or get all events for the user const events = await graph.users.getById('user@tenant.onmicrosoft.com').events(); // You can get my default calendar events const events = await graph.me.calendar.events(); // or get all events for me const events = await graph.me.events();","title":"Get Events For a User's Default Calendar"},{"location":"graph/calendars/#get-events-by-id","text":"You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA=='; const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; // Get events by ID const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID); const events = await graph.me.events.getByID(EventID); // Get an event by ID from a specific calendar const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID); const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);","title":"Get Events By ID"},{"location":"graph/calendars/#create-events","text":"This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add( { \"subject\": \"Let's go for lunch\", \"body\": { \"contentType\": \"HTML\", \"content\": \"Does late morning work for you?\" }, \"start\": { \"dateTime\": \"2017-04-15T12:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"end\": { \"dateTime\": \"2017-04-15T14:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"location\":{ \"displayName\":\"Harry's Bar\" }, \"attendees\": [ { \"emailAddress\": { \"address\":\"samanthab@contoso.onmicrosoft.com\", \"name\": \"Samantha Booth\" }, \"type\": \"required\" } ] });","title":"Create Events"},{"location":"graph/calendars/#update-events","text":"This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({ reminderMinutesBeforeStart: 99, });","title":"Update Events"},{"location":"graph/calendars/#delete-event","text":"This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete(); await graph.me.events.getById(EVENT_ID).delete();","title":"Delete Event"},{"location":"graph/calendars/#get-calendar-for-a-group","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();","title":"Get Calendar for a Group"},{"location":"graph/calendars/#get-events-for-a-group","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; // You can do one of const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events(); // or const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();","title":"Get Events for a Group"},{"location":"graph/calendars/#get-calendar-view","text":"Added in 2.0.7 Gets the events in a calendar during a specified date range. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // basic request, note need to invoke the returned queryable const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\")(); // you can use select, top, etc to filter your returned results const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)(); // you can specify times along with the dates const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")(); const view4 = await graph.me.calendarView(\"2020-01-01\", \"2020-03-01\")();","title":"Get Calendar View"},{"location":"graph/contacts/","text":"@pnp/graph/contacts \u00b6 The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described you can add and edit both contacts and folders in a users Outlook. More information can be found in the official Graph documentation: Contact Resource Type IContact, IContacts, IContactFolder, IContactFolders \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/contacts\"; Preset: All import { graph } from \"@pnp/graph/presets/all\"; Set up notes \u00b6 To make user calls you can use getById where the id is the users email address. Contact ID, Folder ID, and Parent Folder ID use the following format \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\" Get all of the Contacts \u00b6 Gets a list of all the contacts for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts(); const contacts2 = await graph.me.contacts(); Get Contact by Id \u00b6 Gets a specific contact by ID for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)(); const contact2 = await graph.me.contacts.getById(contactID)(); Add a new Contact \u00b6 Adds a new contact for the user. import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); Update a Contact \u00b6 Updates a specific contact by ID for teh designated user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: \"1986-05-30\" }); const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: \"1986-05-30\" }); Delete a Contact \u00b6 Delete a contact from the list of contacts for a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete(); const delContact2 = await graph.me.contacts.getById(contactID).delete(); Get all of the Contact Folders \u00b6 Get all the folders for the designated user's contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders(); const contactFolders2 = await graph.me.contactFolders(); Get Contact Folder by Id \u00b6 Get a contact folder by ID for the specified user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)(); const contactFolder2 = await graph.me.contactFolders.getById(folderID)(); Add a new Contact Folder \u00b6 Add a new folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const parentFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=\"; const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add(\"New Folder\", parentFolderID); const addedContactFolder2 = await graph.me.contactFolders.add(\"New Folder\", parentFolderID); Update a Contact Folder \u00b6 Update an existing folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: \"Updated Folder\" }); const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: \"Updated Folder\" }); Delete a Contact Folder \u00b6 Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete(); const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete(); Get all of the Contacts from the Contact Folder \u00b6 Get all the contacts in a folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts(); const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts(); Get Child Folders of the Contact Folder \u00b6 Get child folders from contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders(); const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders(); Add a new Child Folder \u00b6 Add a new child folder to a contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID); const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID); Get Child Folder by Id \u00b6 Get child folder by ID from user contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)(); const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)(); Add Contact in Child Folder of Contact Folder \u00b6 Add a new contact to a child folder import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"./@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);","title":"contacts"},{"location":"graph/contacts/#pnpgraphcontacts","text":"The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described you can add and edit both contacts and folders in a users Outlook. More information can be found in the official Graph documentation: Contact Resource Type","title":"@pnp/graph/contacts"},{"location":"graph/contacts/#icontact-icontacts-icontactfolder-icontactfolders","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/contacts\"; Preset: All import { graph } from \"@pnp/graph/presets/all\";","title":"IContact, IContacts, IContactFolder, IContactFolders"},{"location":"graph/contacts/#set-up-notes","text":"To make user calls you can use getById where the id is the users email address. Contact ID, Folder ID, and Parent Folder ID use the following format \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"","title":"Set up notes"},{"location":"graph/contacts/#get-all-of-the-contacts","text":"Gets a list of all the contacts for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts(); const contacts2 = await graph.me.contacts();","title":"Get all of the Contacts"},{"location":"graph/contacts/#get-contact-by-id","text":"Gets a specific contact by ID for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)(); const contact2 = await graph.me.contacts.getById(contactID)();","title":"Get Contact by Id"},{"location":"graph/contacts/#add-a-new-contact","text":"Adds a new contact for the user. import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);","title":"Add a new Contact"},{"location":"graph/contacts/#update-a-contact","text":"Updates a specific contact by ID for teh designated user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: \"1986-05-30\" }); const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: \"1986-05-30\" });","title":"Update a Contact"},{"location":"graph/contacts/#delete-a-contact","text":"Delete a contact from the list of contacts for a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete(); const delContact2 = await graph.me.contacts.getById(contactID).delete();","title":"Delete a Contact"},{"location":"graph/contacts/#get-all-of-the-contact-folders","text":"Get all the folders for the designated user's contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders(); const contactFolders2 = await graph.me.contactFolders();","title":"Get all of the Contact Folders"},{"location":"graph/contacts/#get-contact-folder-by-id","text":"Get a contact folder by ID for the specified user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)(); const contactFolder2 = await graph.me.contactFolders.getById(folderID)();","title":"Get Contact Folder by Id"},{"location":"graph/contacts/#add-a-new-contact-folder","text":"Add a new folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const parentFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=\"; const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add(\"New Folder\", parentFolderID); const addedContactFolder2 = await graph.me.contactFolders.add(\"New Folder\", parentFolderID);","title":"Add a new Contact Folder"},{"location":"graph/contacts/#update-a-contact-folder","text":"Update an existing folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: \"Updated Folder\" }); const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: \"Updated Folder\" });","title":"Update a Contact Folder"},{"location":"graph/contacts/#delete-a-contact-folder","text":"Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete(); const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();","title":"Delete a Contact Folder"},{"location":"graph/contacts/#get-all-of-the-contacts-from-the-contact-folder","text":"Get all the contacts in a folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts(); const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();","title":"Get all of the Contacts from the Contact Folder"},{"location":"graph/contacts/#get-child-folders-of-the-contact-folder","text":"Get child folders from contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders(); const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();","title":"Get Child Folders of the Contact Folder"},{"location":"graph/contacts/#add-a-new-child-folder","text":"Add a new child folder to a contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID); const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID);","title":"Add a new Child Folder"},{"location":"graph/contacts/#get-child-folder-by-id","text":"Get child folder by ID from user contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)(); const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();","title":"Get Child Folder by Id"},{"location":"graph/contacts/#add-contact-in-child-folder-of-contact-folder","text":"Add a new contact to a child folder import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"./@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);","title":"Add Contact in Child Folder of Contact Folder"},{"location":"graph/directoryobjects/","text":"@pnp/graph/directoryObjects \u00b6 Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types. More information can be found in the official Graph documentation: DirectoryObject Resource Type IDirectoryObject, IDirectoryObjects \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; Preset: All import { graph } from \"@pnp/sp/presets/all\"; The groups and directory roles for the user \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf(); const memberOf2 = await graph.me.memberOf(); Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/groups\" const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups(); const memberGroups2 = await graph.me.getMemberGroups(); // Returns only security enabled groups const memberGroups3 = await graph.me.getMemberGroups(true); const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups(); Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects(); const memberObjects2 = await graph.me.getMemberObjects(); // Returns only security enabled groups const memberObjects3 = await graph.me.getMemberObjects(true); const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); Check for membership in a specified list of groups \u00b6 And returns from that list those groups of which the specified user, group, or directory object is a member import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers2 = await graph.me.checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); Get directoryObject by Id \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26'); Delete directoryObject \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()","title":"directory objects"},{"location":"graph/directoryobjects/#pnpgraphdirectoryobjects","text":"Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types. More information can be found in the official Graph documentation: DirectoryObject Resource Type","title":"@pnp/graph/directoryObjects"},{"location":"graph/directoryobjects/#idirectoryobject-idirectoryobjects","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; Preset: All import { graph } from \"@pnp/sp/presets/all\";","title":"IDirectoryObject, IDirectoryObjects"},{"location":"graph/directoryobjects/#the-groups-and-directory-roles-for-the-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf(); const memberOf2 = await graph.me.memberOf();","title":"The groups and directory roles for the user"},{"location":"graph/directoryobjects/#return-all-the-groups-the-user-group-or-directoryobject-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/groups\" const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups(); const memberGroups2 = await graph.me.getMemberGroups(); // Returns only security enabled groups const memberGroups3 = await graph.me.getMemberGroups(true); const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();","title":"Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups"},{"location":"graph/directoryobjects/#returns-all-the-groups-administrative-units-and-directory-roles-that-a-user-group-or-directory-object-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects(); const memberObjects2 = await graph.me.getMemberObjects(); // Returns only security enabled groups const memberObjects3 = await graph.me.getMemberObjects(true); const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();","title":"Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups"},{"location":"graph/directoryobjects/#check-for-membership-in-a-specified-list-of-groups","text":"And returns from that list those groups of which the specified user, group, or directory object is a member import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers2 = await graph.me.checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);","title":"Check for membership in a specified list of groups"},{"location":"graph/directoryobjects/#get-directoryobject-by-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');","title":"Get directoryObject by Id"},{"location":"graph/directoryobjects/#delete-directoryobject","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()","title":"Delete directoryObject"},{"location":"graph/groups/","text":"@pnp/graph/groups \u00b6 Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent. Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups. You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation . IGroup, IGroups \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups} from \"@pnp/graph/groups\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; Preset: All import { graph, Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups } from \"@pnp/graph/presets/all\"; Add a Group \u00b6 Add a new group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import { GroupType } from '@pnp/graph/groups'; const groupAddResult = await graph.groups.add(\"GroupName\", \"Mail_NickName\", GroupType.Office365); const group = await groupAddResult.group(); Delete a Group \u00b6 Deletes an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").delete(); Update Group Properties \u00b6 Updates an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").update({ displayName: newName, propertyName: updatedValue}); Add favorite \u00b6 Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").addFavorite(); Remove favorite \u00b6 Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").removeFavorite(); Reset Unseen Count \u00b6 Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").resetUnseenCount(); Subscribe By Mail \u00b6 Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").subscribeByMail(); Unsubscribe By Mail \u00b6 Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").unsubscribeByMail(); Get Calendar View \u00b6 Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; const startDate = new Date(\"2020-04-01\"); const endDate = new Date(\"2020-03-01\"); const events = graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").getCalendarView(startDate, endDate); Group Photo Operations \u00b6 See Photos Get the Team Site for a Group \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/sites/group\"; const teamSite = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").sites.root(); const url = teamSite.webUrl","title":"groups"},{"location":"graph/groups/#pnpgraphgroups","text":"Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent. Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups. You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/groups"},{"location":"graph/groups/#igroup-igroups","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups} from \"@pnp/graph/groups\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; Preset: All import { graph, Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups } from \"@pnp/graph/presets/all\";","title":"IGroup, IGroups"},{"location":"graph/groups/#add-a-group","text":"Add a new group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import { GroupType } from '@pnp/graph/groups'; const groupAddResult = await graph.groups.add(\"GroupName\", \"Mail_NickName\", GroupType.Office365); const group = await groupAddResult.group();","title":"Add a Group"},{"location":"graph/groups/#delete-a-group","text":"Deletes an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").delete();","title":"Delete a Group"},{"location":"graph/groups/#update-group-properties","text":"Updates an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").update({ displayName: newName, propertyName: updatedValue});","title":"Update Group Properties"},{"location":"graph/groups/#add-favorite","text":"Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").addFavorite();","title":"Add favorite"},{"location":"graph/groups/#remove-favorite","text":"Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").removeFavorite();","title":"Remove favorite"},{"location":"graph/groups/#reset-unseen-count","text":"Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").resetUnseenCount();","title":"Reset Unseen Count"},{"location":"graph/groups/#subscribe-by-mail","text":"Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").subscribeByMail();","title":"Subscribe By Mail"},{"location":"graph/groups/#unsubscribe-by-mail","text":"Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").unsubscribeByMail();","title":"Unsubscribe By Mail"},{"location":"graph/groups/#get-calendar-view","text":"Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; const startDate = new Date(\"2020-04-01\"); const endDate = new Date(\"2020-03-01\"); const events = graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").getCalendarView(startDate, endDate);","title":"Get Calendar View"},{"location":"graph/groups/#group-photo-operations","text":"See Photos","title":"Group Photo Operations"},{"location":"graph/groups/#get-the-team-site-for-a-group","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/sites/group\"; const teamSite = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").sites.root(); const url = teamSite.webUrl","title":"Get the Team Site for a Group"},{"location":"graph/insights/","text":"@pnp/graph/insights \u00b6 This module helps you get Insights in form of Trending , Used and Shared . The results are based on relationships calculated using advanced analytics and machine learning techniques. IInsights \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; Preset: All import \"@pnp/graph/presets/all\"; Get all Trending documents \u00b6 Returns documents from OneDrive and SharePoint sites trending around a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trending = await graph.me.insights.trending() const trending = await graph.users.getById(\"userId\").insights.trending() Get a Trending document by Id \u00b6 Using the getById method to get a trending document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trendingDoc = await graph.me.insights.trending.getById('Id')() const trendingDoc = await graph.users.getById(\"userId\").insights.trending.getById('Id')() Get the resource from Trending document \u00b6 Using the resources method to get the resource from a trending document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.trending.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.trending.getById('Id').resource() Get all Used documents \u00b6 Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const used = await graph.me.insights.used() const used = await graph.users.getById(\"userId\").insights.used() Get a Used document by Id \u00b6 Using the getById method to get a used document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const usedDoc = await graph.me.insights.used.getById('Id')() const usedDoc = await graph.users.getById(\"userId\").insights.used.getById('Id')() Get the resource from Used document \u00b6 Using the resources method to get the resource from a used document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.used.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.used.getById('Id').resource() Get all Shared documents \u00b6 Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const shared = await graph.me.insights.shared() const shared = await graph.users.getById(\"userId\").insights.shared() Get a Shared document by Id \u00b6 Using the getById method to get a shared document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const sharedDoc = await graph.me.insights.shared.getById('Id')() const sharedDoc = await graph.users.getById(\"userId\").insights.shared.getById('Id')() Get the resource from a Shared document \u00b6 Using the resources method to get the resource from a shared document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.shared.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.shared.getById('Id').resource()","title":"insights"},{"location":"graph/insights/#pnpgraphinsights","text":"This module helps you get Insights in form of Trending , Used and Shared . The results are based on relationships calculated using advanced analytics and machine learning techniques.","title":"@pnp/graph/insights"},{"location":"graph/insights/#iinsights","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInsights"},{"location":"graph/insights/#get-all-trending-documents","text":"Returns documents from OneDrive and SharePoint sites trending around a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trending = await graph.me.insights.trending() const trending = await graph.users.getById(\"userId\").insights.trending()","title":"Get all Trending documents"},{"location":"graph/insights/#get-a-trending-document-by-id","text":"Using the getById method to get a trending document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trendingDoc = await graph.me.insights.trending.getById('Id')() const trendingDoc = await graph.users.getById(\"userId\").insights.trending.getById('Id')()","title":"Get a Trending document by Id"},{"location":"graph/insights/#get-the-resource-from-trending-document","text":"Using the resources method to get the resource from a trending document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.trending.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.trending.getById('Id').resource()","title":"Get the resource from Trending document"},{"location":"graph/insights/#get-all-used-documents","text":"Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const used = await graph.me.insights.used() const used = await graph.users.getById(\"userId\").insights.used()","title":"Get all Used documents"},{"location":"graph/insights/#get-a-used-document-by-id","text":"Using the getById method to get a used document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const usedDoc = await graph.me.insights.used.getById('Id')() const usedDoc = await graph.users.getById(\"userId\").insights.used.getById('Id')()","title":"Get a Used document by Id"},{"location":"graph/insights/#get-the-resource-from-used-document","text":"Using the resources method to get the resource from a used document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.used.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.used.getById('Id').resource()","title":"Get the resource from Used document"},{"location":"graph/insights/#get-all-shared-documents","text":"Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const shared = await graph.me.insights.shared() const shared = await graph.users.getById(\"userId\").insights.shared()","title":"Get all Shared documents"},{"location":"graph/insights/#get-a-shared-document-by-id","text":"Using the getById method to get a shared document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const sharedDoc = await graph.me.insights.shared.getById('Id')() const sharedDoc = await graph.users.getById(\"userId\").insights.shared.getById('Id')()","title":"Get a Shared document by Id"},{"location":"graph/insights/#get-the-resource-from-a-shared-document","text":"Using the resources method to get the resource from a shared document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.shared.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.shared.getById('Id').resource()","title":"Get the resource from a Shared document"},{"location":"graph/invitations/","text":"@pnp/graph/invitations \u00b6 The ability invite an external user via the invitation manager IInvitations \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\"; Preset: All import \"@pnp/graph/presets/all\"; Create Invitation \u00b6 Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\" const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');","title":"invitations"},{"location":"graph/invitations/#pnpgraphinvitations","text":"The ability invite an external user via the invitation manager","title":"@pnp/graph/invitations"},{"location":"graph/invitations/#iinvitations","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInvitations"},{"location":"graph/invitations/#create-invitation","text":"Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\" const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');","title":"Create Invitation"},{"location":"graph/onedrive/","text":"@pnp/graph/onedrive \u00b6 The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive. IInvitations \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; Preset: All import \"@pnp/graph/presets/all\"; Get the default drive \u00b6 Using the drive() you can get the default drive from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives(); Get all of the drives \u00b6 Using the drives() you can get the users available drives from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives(); Get drive by Id \u00b6 Using the drives.getById() you can get one of the available drives in Outlook import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId'); const drive = await graph.me.drives.getById('driveId'); Get the associated list of a drive \u00b6 Using the list() you get the associated list import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list(); const list = await graph.me.drives.getById('driveId').list(); Get the recent files \u00b6 Using the recent() you get the recent files import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent(); const files = await graph.me.drives.getById('driveId').recent(); Get the files shared with me \u00b6 Using the sharedWithMe() you get the files shared with the user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe(); const shared = await graph.me.drives.getById('driveId').sharedWithMe(); Get the Root folder \u00b6 Using the root() you get the root folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root(); const root = await graph.me.drives.getById('driveId').root(); Get the Children \u00b6 Using the children() you get the children import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children(); const rootChildren = await graph.me.drives.getById('driveId').root.children(); const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children(); const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children(); Add folder or item \u00b6 Using the add you can add a folder or an item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; import { DriveItem as IDriveItem } from \"@microsoft/microsoft-graph-types\"; const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', {folder: {}}); const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', {folder: {}}); Search items \u00b6 Using the search() you can search for items, and optionally select properties import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText')(); const search = await graph.me.drives.getById('driveId')root.search('queryText')(); Get specific item in drive \u00b6 Using the items.getById() you can get a specific item from the current drive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId'); const item = await graph.me.drives.getById('driveId').items.getById('itemId'); Get thumbnails \u00b6 Using the thumbnails() you get the thumbnails import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails(); Delete drive item \u00b6 Using the delete() you delete the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete(); Update drive item \u00b6 Using the update() you update the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"}); const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"}); Move drive item \u00b6 Using the move() you move the current item, and optionally update it import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; // Requires a parentReference to the new folder location const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"}); const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"});","title":"onedrive"},{"location":"graph/onedrive/#pnpgraphonedrive","text":"The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive.","title":"@pnp/graph/onedrive"},{"location":"graph/onedrive/#iinvitations","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInvitations"},{"location":"graph/onedrive/#get-the-default-drive","text":"Using the drive() you can get the default drive from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives();","title":"Get the default drive"},{"location":"graph/onedrive/#get-all-of-the-drives","text":"Using the drives() you can get the users available drives from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives();","title":"Get all of the drives"},{"location":"graph/onedrive/#get-drive-by-id","text":"Using the drives.getById() you can get one of the available drives in Outlook import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId'); const drive = await graph.me.drives.getById('driveId');","title":"Get drive by Id"},{"location":"graph/onedrive/#get-the-associated-list-of-a-drive","text":"Using the list() you get the associated list import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list(); const list = await graph.me.drives.getById('driveId').list();","title":"Get the associated list of a drive"},{"location":"graph/onedrive/#get-the-recent-files","text":"Using the recent() you get the recent files import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent(); const files = await graph.me.drives.getById('driveId').recent();","title":"Get the recent files"},{"location":"graph/onedrive/#get-the-files-shared-with-me","text":"Using the sharedWithMe() you get the files shared with the user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe(); const shared = await graph.me.drives.getById('driveId').sharedWithMe();","title":"Get the files shared with me"},{"location":"graph/onedrive/#get-the-root-folder","text":"Using the root() you get the root folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root(); const root = await graph.me.drives.getById('driveId').root();","title":"Get the Root folder"},{"location":"graph/onedrive/#get-the-children","text":"Using the children() you get the children import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children(); const rootChildren = await graph.me.drives.getById('driveId').root.children(); const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children(); const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children();","title":"Get the Children"},{"location":"graph/onedrive/#add-folder-or-item","text":"Using the add you can add a folder or an item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; import { DriveItem as IDriveItem } from \"@microsoft/microsoft-graph-types\"; const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', {folder: {}}); const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', {folder: {}});","title":"Add folder or item"},{"location":"graph/onedrive/#search-items","text":"Using the search() you can search for items, and optionally select properties import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText')(); const search = await graph.me.drives.getById('driveId')root.search('queryText')();","title":"Search items"},{"location":"graph/onedrive/#get-specific-item-in-drive","text":"Using the items.getById() you can get a specific item from the current drive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId'); const item = await graph.me.drives.getById('driveId').items.getById('itemId');","title":"Get specific item in drive"},{"location":"graph/onedrive/#get-thumbnails","text":"Using the thumbnails() you get the thumbnails import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails();","title":"Get thumbnails"},{"location":"graph/onedrive/#delete-drive-item","text":"Using the delete() you delete the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete();","title":"Delete drive item"},{"location":"graph/onedrive/#update-drive-item","text":"Using the update() you update the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"}); const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"});","title":"Update drive item"},{"location":"graph/onedrive/#move-drive-item","text":"Using the move() you move the current item, and optionally update it import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; // Requires a parentReference to the new folder location const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"}); const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"});","title":"Move drive item"},{"location":"graph/outlook/","text":"@pnp/graph/outlook \u00b6 Represents the Outlook services available to a user. Currently, only interacting with categories is supported. You can learn more by reading the Official Microsoft Graph Documentation . IUsers, IUser, IPeople \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Outlook, IOutlook, MasterCategories, IMasterCategories, OutlookCategory, IOutlookCategory} from \"@pnp/graph/outlook\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/outlook\"; Preset: All import { graph, Outlook, IOutlook, MasterCategories, IMasterCategories } from \"@pnp/graph/presets/all\"; Get All Categories User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories(); Add Category User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions await graph.me.outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' }); // Application permissions await graph.users.getById('{user id}').outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' }); Update Category \u00b6 Testing has shown that displayName cannot be updated. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; import { OutlookCategory } from \"@microsoft/microsoft-graph-types\"; const categoryUpdate: OutlookCategory = { color: \"preset5\" } // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate); Delete Category \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();","title":"outlook"},{"location":"graph/outlook/#pnpgraphoutlook","text":"Represents the Outlook services available to a user. Currently, only interacting with categories is supported. You can learn more by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/outlook"},{"location":"graph/outlook/#iusers-iuser-ipeople","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Outlook, IOutlook, MasterCategories, IMasterCategories, OutlookCategory, IOutlookCategory} from \"@pnp/graph/outlook\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/outlook\"; Preset: All import { graph, Outlook, IOutlook, MasterCategories, IMasterCategories } from \"@pnp/graph/presets/all\";","title":"IUsers, IUser, IPeople"},{"location":"graph/outlook/#get-all-categories-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories();","title":"Get All Categories User"},{"location":"graph/outlook/#add-category-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions await graph.me.outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' }); // Application permissions await graph.users.getById('{user id}').outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' });","title":"Add Category User"},{"location":"graph/outlook/#update-category","text":"Testing has shown that displayName cannot be updated. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; import { OutlookCategory } from \"@microsoft/microsoft-graph-types\"; const categoryUpdate: OutlookCategory = { color: \"preset5\" } // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);","title":"Update Category"},{"location":"graph/outlook/#delete-category","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();","title":"Delete Category"},{"location":"graph/photos/","text":"@pnp/graph/photos \u00b6 A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation . IPhoto \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IPhoto, Photo} from \"@pnp/graph/photos\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/photos\"; Preset: All import { graph, IPhoto, Photo } from \"@pnp/sp/presets/all\"; Current User Photo \u00b6 This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const photoValue = await graph.me.photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl); Current Group Photo \u00b6 This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/photos\"; const photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl); Set User Photo \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file); Set Group Photo \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file);","title":"photos"},{"location":"graph/photos/#pnpgraphphotos","text":"A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/photos"},{"location":"graph/photos/#iphoto","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IPhoto, Photo} from \"@pnp/graph/photos\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/photos\"; Preset: All import { graph, IPhoto, Photo } from \"@pnp/sp/presets/all\";","title":"IPhoto"},{"location":"graph/photos/#current-user-photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const photoValue = await graph.me.photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);","title":"Current User Photo"},{"location":"graph/photos/#current-group-photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/photos\"; const photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);","title":"Current Group Photo"},{"location":"graph/photos/#set-user-photo","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file);","title":"Set User Photo"},{"location":"graph/photos/#set-group-photo","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file);","title":"Set Group Photo"},{"location":"graph/planner/","text":"@pnp/graph/planner \u00b6 The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner. IInvitations \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\"; Preset: All import \"@pnp/graph/presets/all\"; Get Plans by Id \u00b6 Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const plan = await graph.planner.plans.getById('planId')(); Add new Plan \u00b6 Using the planner.plans.add() you can create a new Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newPlan = await graph.planner.plans.add('groupObjectId', 'title'); Get Tasks in Plan \u00b6 Using the tasks() you can get the Tasks in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planTasks = await graph.planner.plans.getById('planId').tasks(); Get Buckets in Plan \u00b6 Using the buckets() you can get the Buckets in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planBuckets = await graph.planner.plans.getById('planId').buckets(); Get Details in Plan \u00b6 Using the details() you can get the details in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planDetails = await graph.planner.plans.getById('planId').details(); Delete Plan \u00b6 Using the delete() you can get delete a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delPlan = await graph.planner.plans.getById('planId').delete('planEtag'); Update Plan \u00b6 Using the update() you can get update a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'}); Get Task by Id \u00b6 Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const task = await graph.planner.tasks.getById('taskId')(); Add new Task \u00b6 Using the planner.tasks.add() you can create a new Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newTask = await graph.planner.tasks.add('planId', 'title'); Get Details in Task \u00b6 Using the details() you can get the details in a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const taskDetails = await graph.planner.tasks.getById('taskId').details(); Delete Task \u00b6 Using the delete() you can get delete a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag'); Update Task \u00b6 Using the update() you can get update a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'}); Get Buckets by Id \u00b6 Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucket = await graph.planner.buckets.getById('bucketId')(); Add new Bucket \u00b6 Using the planner.buckets.add() you can create a new Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newBucket = await graph.planner.buckets.add('name', 'planId'); Update Bucket \u00b6 Using the update() you can get update a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updBucket = await graph.planner.buckets.getById('bucketId').update({name: \"Name\", eTag:'bucketEtag'}); Delete Bucket \u00b6 Using the delete() you can get delete a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag'); Get Bucket Tasks \u00b6 Using the tasks() you can get Tasks in a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();","title":"planner"},{"location":"graph/planner/#pnpgraphplanner","text":"The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner.","title":"@pnp/graph/planner"},{"location":"graph/planner/#iinvitations","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInvitations"},{"location":"graph/planner/#get-plans-by-id","text":"Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const plan = await graph.planner.plans.getById('planId')();","title":"Get Plans by Id"},{"location":"graph/planner/#add-new-plan","text":"Using the planner.plans.add() you can create a new Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newPlan = await graph.planner.plans.add('groupObjectId', 'title');","title":"Add new Plan"},{"location":"graph/planner/#get-tasks-in-plan","text":"Using the tasks() you can get the Tasks in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planTasks = await graph.planner.plans.getById('planId').tasks();","title":"Get Tasks in Plan"},{"location":"graph/planner/#get-buckets-in-plan","text":"Using the buckets() you can get the Buckets in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planBuckets = await graph.planner.plans.getById('planId').buckets();","title":"Get Buckets in Plan"},{"location":"graph/planner/#get-details-in-plan","text":"Using the details() you can get the details in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planDetails = await graph.planner.plans.getById('planId').details();","title":"Get Details in Plan"},{"location":"graph/planner/#delete-plan","text":"Using the delete() you can get delete a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delPlan = await graph.planner.plans.getById('planId').delete('planEtag');","title":"Delete Plan"},{"location":"graph/planner/#update-plan","text":"Using the update() you can get update a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});","title":"Update Plan"},{"location":"graph/planner/#get-task-by-id","text":"Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const task = await graph.planner.tasks.getById('taskId')();","title":"Get Task by Id"},{"location":"graph/planner/#add-new-task","text":"Using the planner.tasks.add() you can create a new Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newTask = await graph.planner.tasks.add('planId', 'title');","title":"Add new Task"},{"location":"graph/planner/#get-details-in-task","text":"Using the details() you can get the details in a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const taskDetails = await graph.planner.tasks.getById('taskId').details();","title":"Get Details in Task"},{"location":"graph/planner/#delete-task","text":"Using the delete() you can get delete a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');","title":"Delete Task"},{"location":"graph/planner/#update-task","text":"Using the update() you can get update a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});","title":"Update Task"},{"location":"graph/planner/#get-buckets-by-id","text":"Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucket = await graph.planner.buckets.getById('bucketId')();","title":"Get Buckets by Id"},{"location":"graph/planner/#add-new-bucket","text":"Using the planner.buckets.add() you can create a new Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newBucket = await graph.planner.buckets.add('name', 'planId');","title":"Add new Bucket"},{"location":"graph/planner/#update-bucket","text":"Using the update() you can get update a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updBucket = await graph.planner.buckets.getById('bucketId').update({name: \"Name\", eTag:'bucketEtag'});","title":"Update Bucket"},{"location":"graph/planner/#delete-bucket","text":"Using the delete() you can get delete a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');","title":"Delete Bucket"},{"location":"graph/planner/#get-bucket-tasks","text":"Using the tasks() you can get Tasks in a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();","title":"Get Bucket Tasks"},{"location":"graph/search/","text":"@pnp/graph/search \u00b6 The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below. Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; Preset: All import \"@pnp/graph/presets/all\"; Call graph.query \u00b6 This example shows calling the search API via the query method of the root graph object. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; const results = await graph.query({ entityTypes: [\"site\"], query: { queryString: \"test\" }, }); Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.","title":"search"},{"location":"graph/search/#pnpgraphsearch","text":"The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below. Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; Preset: All import \"@pnp/graph/presets/all\";","title":"@pnp/graph/search"},{"location":"graph/search/#call-graphquery","text":"This example shows calling the search API via the query method of the root graph object. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; const results = await graph.query({ entityTypes: [\"site\"], query: { queryString: \"test\" }, }); Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.","title":"Call graph.query"},{"location":"graph/subscriptions/","text":"@pnp/graph/subscriptions \u00b6 The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: Mail, events, and contacts from Outlook. Conversations from Office Groups. Drive root items from OneDrive. Users and Groups from Azure Active Directory. Alerts from the Microsoft Graph Security API. Get all of the Subscriptions \u00b6 Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscriptions = await graph.subscriptions(); Create a new Subscription \u00b6 Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const addedSubscription = await graph.subscriptions.add(\"created,updated\", \"https://webhook.azurewebsites.net/api/send/myNotifyClient\", \"me/mailFolders('Inbox')/messages\", \"2019-11-20T18:23:45.9356913Z\"); Get Subscription by Id \u00b6 Using the subscriptions.getById() you can get one of the subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscription = await graph.subscriptions.getById('subscriptionId')(); Delete a Subscription \u00b6 Using the subscriptions.getById().delete() you can remove one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const delSubscription = await graph.subscriptions.getById('subscriptionId').delete(); Update a Subscription \u00b6 Using the subscriptions.getById().update() you can update one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: \"created,updated,deleted\" });","title":"subscriptions"},{"location":"graph/subscriptions/#pnpgraphsubscriptions","text":"The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: Mail, events, and contacts from Outlook. Conversations from Office Groups. Drive root items from OneDrive. Users and Groups from Azure Active Directory. Alerts from the Microsoft Graph Security API.","title":"@pnp/graph/subscriptions"},{"location":"graph/subscriptions/#get-all-of-the-subscriptions","text":"Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscriptions = await graph.subscriptions();","title":"Get all of the Subscriptions"},{"location":"graph/subscriptions/#create-a-new-subscription","text":"Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const addedSubscription = await graph.subscriptions.add(\"created,updated\", \"https://webhook.azurewebsites.net/api/send/myNotifyClient\", \"me/mailFolders('Inbox')/messages\", \"2019-11-20T18:23:45.9356913Z\");","title":"Create a new Subscription"},{"location":"graph/subscriptions/#get-subscription-by-id","text":"Using the subscriptions.getById() you can get one of the subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscription = await graph.subscriptions.getById('subscriptionId')();","title":"Get Subscription by Id"},{"location":"graph/subscriptions/#delete-a-subscription","text":"Using the subscriptions.getById().delete() you can remove one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const delSubscription = await graph.subscriptions.getById('subscriptionId').delete();","title":"Delete a Subscription"},{"location":"graph/subscriptions/#update-a-subscription","text":"Using the subscriptions.getById().update() you can update one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: \"created,updated,deleted\" });","title":"Update a Subscription"},{"location":"graph/teams/","text":"@pnp/graph/teams \u00b6 The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams. Teams the user is a member of \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/teams\" const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams(); const myJoinedTeams = await graph.me.joinedTeams(); Get Teams by Id \u00b6 Using the teams.getById() you can get a specific Team. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')(); Create new Team/Group - Method #1 \u00b6 The first way to create a new Team and corresponding Group is to first create the group and then create the team. Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group. Create a Team via a specific group \u00b6 Here we get the group via id and use createTeam import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" import \"@pnp/graph/groups\" const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ \"memberSettings\": { \"allowCreateUpdateChannels\": true }, \"messagingSettings\": { \"allowUserEditMessages\": true, \"allowUserDeleteMessages\": true }, \"funSettings\": { \"allowGiphy\": true, \"giphyContentRating\": \"strict\" }}); Create new Team/Group - Method #2 \u00b6 The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = { \"template@odata.bind\": \"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\", \"displayName\": \"PnPJS Test Team\", \"description\": \"PnPJS Test Team\u2019s Description\", \"members\": [ { \"@odata.type\": \"#microsoft.graph.aadUserConversationMember\", \"roles\": [\"owner\"], \"user@odata.bind\": \"https://graph.microsoft.com/v1.0/users('{owners user id}')\", }, ], }; const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team); //To check the status of the team creation, call getOperationById for the newly created team. const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId); Clone a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public'); Get Teams Async Operation \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public'); const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId); Archive a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive(); Unarchive a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive(); Get all channels of a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels(); Get channel by Id \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')(); Create a new Channel \u00b6 import { graph } from \"@pnp/graph\"; const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description'); Get installed Apps \u00b6 import { graph } from \"@pnp/graph\"; const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps(); Add an App \u00b6 import { graph } from \"@pnp/graph\"; const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a'); Remove an App \u00b6 import { graph } from \"@pnp/graph\"; const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove(); Get Tabs from a Channel \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs(); Get Tab by Id \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')(); Add a new Tab \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',{});","title":"teams"},{"location":"graph/teams/#pnpgraphteams","text":"The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams.","title":"@pnp/graph/teams"},{"location":"graph/teams/#teams-the-user-is-a-member-of","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/teams\" const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams(); const myJoinedTeams = await graph.me.joinedTeams();","title":"Teams the user is a member of"},{"location":"graph/teams/#get-teams-by-id","text":"Using the teams.getById() you can get a specific Team. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();","title":"Get Teams by Id"},{"location":"graph/teams/#create-new-teamgroup-method-1","text":"The first way to create a new Team and corresponding Group is to first create the group and then create the team. Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.","title":"Create new Team/Group - Method #1"},{"location":"graph/teams/#create-a-team-via-a-specific-group","text":"Here we get the group via id and use createTeam import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" import \"@pnp/graph/groups\" const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ \"memberSettings\": { \"allowCreateUpdateChannels\": true }, \"messagingSettings\": { \"allowUserEditMessages\": true, \"allowUserDeleteMessages\": true }, \"funSettings\": { \"allowGiphy\": true, \"giphyContentRating\": \"strict\" }});","title":"Create a Team via a specific group"},{"location":"graph/teams/#create-new-teamgroup-method-2","text":"The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = { \"template@odata.bind\": \"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\", \"displayName\": \"PnPJS Test Team\", \"description\": \"PnPJS Test Team\u2019s Description\", \"members\": [ { \"@odata.type\": \"#microsoft.graph.aadUserConversationMember\", \"roles\": [\"owner\"], \"user@odata.bind\": \"https://graph.microsoft.com/v1.0/users('{owners user id}')\", }, ], }; const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team); //To check the status of the team creation, call getOperationById for the newly created team. const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);","title":"Create new Team/Group - Method #2"},{"location":"graph/teams/#clone-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public');","title":"Clone a Team"},{"location":"graph/teams/#get-teams-async-operation","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public'); const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);","title":"Get Teams Async Operation"},{"location":"graph/teams/#archive-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();","title":"Archive a Team"},{"location":"graph/teams/#unarchive-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();","title":"Unarchive a Team"},{"location":"graph/teams/#get-all-channels-of-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();","title":"Get all channels of a Team"},{"location":"graph/teams/#get-channel-by-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();","title":"Get channel by Id"},{"location":"graph/teams/#create-a-new-channel","text":"import { graph } from \"@pnp/graph\"; const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');","title":"Create a new Channel"},{"location":"graph/teams/#get-installed-apps","text":"import { graph } from \"@pnp/graph\"; const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();","title":"Get installed Apps"},{"location":"graph/teams/#add-an-app","text":"import { graph } from \"@pnp/graph\"; const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');","title":"Add an App"},{"location":"graph/teams/#remove-an-app","text":"import { graph } from \"@pnp/graph\"; const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove();","title":"Remove an App"},{"location":"graph/teams/#get-tabs-from-a-channel","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();","title":"Get Tabs from a Channel"},{"location":"graph/teams/#get-tab-by-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();","title":"Get Tab by Id"},{"location":"graph/teams/#add-a-new-tab","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',{});","title":"Add a new Tab"},{"location":"graph/users/","text":"@pnp/graph/users \u00b6 Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation . IUsers, IUser, IPeople \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IUser, IUsers, User, Users, IPeople, People} from \"@pnp/graph/users\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; Preset: All import { graph,IUser, IUsers, User, Users, IPeople, People } from \"@pnp/graph/presets/all\"; Current User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const currentUser = await graph.me(); Get All Users in the Organization \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const allUsers = await graph.users(); Get a User by email address (or user id) \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const matchingUser = await graph.users.getById('jane@contoso.com')(); Update Current User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; await graph.me.update({ displayName: 'John Doe' }); People \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)(); People \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)(); Manager \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const manager = await graph.me.manager(); Direct Reports \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const reports = await graph.me.directReports(); Photo \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const currentUser = await graph.me.photo(); const specificUser = await graph.users.getById('jane@contoso.com').photo(); User Photo Operations \u00b6 See Photos","title":"users"},{"location":"graph/users/#pnpgraphusers","text":"Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/users"},{"location":"graph/users/#iusers-iuser-ipeople","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IUser, IUsers, User, Users, IPeople, People} from \"@pnp/graph/users\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; Preset: All import { graph,IUser, IUsers, User, Users, IPeople, People } from \"@pnp/graph/presets/all\";","title":"IUsers, IUser, IPeople"},{"location":"graph/users/#current-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const currentUser = await graph.me();","title":"Current User"},{"location":"graph/users/#get-all-users-in-the-organization","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const allUsers = await graph.users();","title":"Get All Users in the Organization"},{"location":"graph/users/#get-a-user-by-email-address-or-user-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const matchingUser = await graph.users.getById('jane@contoso.com')();","title":"Get a User by email address (or user id)"},{"location":"graph/users/#update-current-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; await graph.me.update({ displayName: 'John Doe' });","title":"Update Current User"},{"location":"graph/users/#people","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)();","title":"People"},{"location":"graph/users/#people_1","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)();","title":"People"},{"location":"graph/users/#manager","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const manager = await graph.me.manager();","title":"Manager"},{"location":"graph/users/#direct-reports","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const reports = await graph.me.directReports();","title":"Direct Reports"},{"location":"graph/users/#photo","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const currentUser = await graph.me.photo(); const specificUser = await graph.users.getById('jane@contoso.com').photo();","title":"Photo"},{"location":"graph/users/#user-photo-operations","text":"See Photos","title":"User Photo Operations"},{"location":"logging/","text":"@pnp/logging \u00b6 The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers. Getting Started \u00b6 Install the logging module, it has no other dependencies npm install @pnp/logging --save Understanding the Logging Framework \u00b6 The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter. /** * Interface that defines a log listener * */ export interface ILogListener { /** * Any associated data that a given logging listener may choose to log or ignore * * @param entry The information to be logged */ log(entry: ILogEntry): void; } /** * Interface that defines a log entry * */ export interface ILogEntry { /** * The main message to be logged */ message: string; /** * The level of information this message represents */ level: LogLevel; /** * Any associated data that a given logging listener may choose to log or ignore */ data?: any; } Log Levels \u00b6 export const enum LogLevel { Verbose = 0, Info = 1, Warning = 2, Error = 3, Off = 99, } Writing to the Logger \u00b6 To write information to a logger you can use either write, writeJSON, or log. import { Logger, LogLevel } from \"@pnp/logging\"; // write logs a simple string as the message value of the LogEntry Logger.write(\"This is logging a simple string\"); // optionally passing a level, default level is Verbose Logger.write(\"This is logging a simple string\", LogLevel.Error); // this will convert the object to a string using JSON.stringify and set the message with the result Logger.writeJSON({ name: \"value\", name2: \"value2\"}); // optionally passing a level, default level is Verbose Logger.writeJSON({ name: \"value\", name2: \"value2\"}, LogLevel.Warning); // specify the entire LogEntry interface using log Logger.log({ data: { name: \"value\", name2: \"value2\"}, level: LogLevel.Warning, message: \"This is my message\" }); Log an error \u00b6 There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance passed in, the level will be 'Error', and the message will be the Error instance's message property. const e = Error(\"An Error\"); Logger.error(e); Subscribing a Listener \u00b6 By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; // subscribe a listener Logger.subscribe(new ConsoleListener()); // set the active log level Logger.activeLogLevel = LogLevel.Info; Available Listeners \u00b6 There are two listeners included in the library, ConsoleListener and FunctionListener. ConsoleListener \u00b6 This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above. Configuration Options \u00b6 Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel). Using a Prefix \u00b6 To add a prefix to all output, supply a string in the constructor: import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE)); Logger.activeLogLevel = LogLevel.Info; With the above configuration, Logger.write(\"My special message\"); will be output to the console as: MyAwesomeWebPart - My special message Customizing Text Color \u00b6 You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color). Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.): import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'})); Logger.activeLogLevel = LogLevel.Info; With the above configuration: Logger.write(\"My special message\"); Logger.write(\"A warning!\", LogLevel.Warning); Will result in messages that look like this: Color options: color : Default text color for all logging levels unless they're specified verboseColor : Text color to use for messages with LogLevel.Verbose infoColor : Text color to use for messages with LogLevel.Info warningColor : Text color to use for messages with LogLevel.Warning errorColor : Text color to use for messages with LogLevel.Error To set colors without a prefix, specify either undefined or an empty string for the first parameter: Logger.subscribe(new ConsoleListener(undefined, {color:'purple'})); FunctionListener \u00b6 The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. import { Logger, FunctionListener, ILogEntry } from \"@pnp/logging\"; let listener = new FunctionListener((entry: ILogEntry) => { // pass all logging data to an existing framework MyExistingCompanyLoggingFramework.log(entry.message); }); Logger.subscribe(listener); Create a Custom Listener \u00b6 If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface. import { Logger, ILogListener, ILogEntry } from \"@pnp/logging\"; class MyListener implements ILogListener { log(entry: ILogEntry): void { // here you would do something with the entry } } Logger.subscribe(new MyListener());","title":"logging"},{"location":"logging/#pnplogging","text":"The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.","title":"@pnp/logging"},{"location":"logging/#getting-started","text":"Install the logging module, it has no other dependencies npm install @pnp/logging --save","title":"Getting Started"},{"location":"logging/#understanding-the-logging-framework","text":"The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter. /** * Interface that defines a log listener * */ export interface ILogListener { /** * Any associated data that a given logging listener may choose to log or ignore * * @param entry The information to be logged */ log(entry: ILogEntry): void; } /** * Interface that defines a log entry * */ export interface ILogEntry { /** * The main message to be logged */ message: string; /** * The level of information this message represents */ level: LogLevel; /** * Any associated data that a given logging listener may choose to log or ignore */ data?: any; }","title":"Understanding the Logging Framework"},{"location":"logging/#log-levels","text":"export const enum LogLevel { Verbose = 0, Info = 1, Warning = 2, Error = 3, Off = 99, }","title":"Log Levels"},{"location":"logging/#writing-to-the-logger","text":"To write information to a logger you can use either write, writeJSON, or log. import { Logger, LogLevel } from \"@pnp/logging\"; // write logs a simple string as the message value of the LogEntry Logger.write(\"This is logging a simple string\"); // optionally passing a level, default level is Verbose Logger.write(\"This is logging a simple string\", LogLevel.Error); // this will convert the object to a string using JSON.stringify and set the message with the result Logger.writeJSON({ name: \"value\", name2: \"value2\"}); // optionally passing a level, default level is Verbose Logger.writeJSON({ name: \"value\", name2: \"value2\"}, LogLevel.Warning); // specify the entire LogEntry interface using log Logger.log({ data: { name: \"value\", name2: \"value2\"}, level: LogLevel.Warning, message: \"This is my message\" });","title":"Writing to the Logger"},{"location":"logging/#log-an-error","text":"There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance passed in, the level will be 'Error', and the message will be the Error instance's message property. const e = Error(\"An Error\"); Logger.error(e);","title":"Log an error"},{"location":"logging/#subscribing-a-listener","text":"By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; // subscribe a listener Logger.subscribe(new ConsoleListener()); // set the active log level Logger.activeLogLevel = LogLevel.Info;","title":"Subscribing a Listener"},{"location":"logging/#available-listeners","text":"There are two listeners included in the library, ConsoleListener and FunctionListener.","title":"Available Listeners"},{"location":"logging/#consolelistener","text":"This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.","title":"ConsoleListener"},{"location":"logging/#configuration-options","text":"Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).","title":"Configuration Options"},{"location":"logging/#using-a-prefix","text":"To add a prefix to all output, supply a string in the constructor: import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE)); Logger.activeLogLevel = LogLevel.Info; With the above configuration, Logger.write(\"My special message\"); will be output to the console as: MyAwesomeWebPart - My special message","title":"Using a Prefix"},{"location":"logging/#customizing-text-color","text":"You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color). Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.): import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'})); Logger.activeLogLevel = LogLevel.Info; With the above configuration: Logger.write(\"My special message\"); Logger.write(\"A warning!\", LogLevel.Warning); Will result in messages that look like this: Color options: color : Default text color for all logging levels unless they're specified verboseColor : Text color to use for messages with LogLevel.Verbose infoColor : Text color to use for messages with LogLevel.Info warningColor : Text color to use for messages with LogLevel.Warning errorColor : Text color to use for messages with LogLevel.Error To set colors without a prefix, specify either undefined or an empty string for the first parameter: Logger.subscribe(new ConsoleListener(undefined, {color:'purple'}));","title":"Customizing Text Color"},{"location":"logging/#functionlistener","text":"The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. import { Logger, FunctionListener, ILogEntry } from \"@pnp/logging\"; let listener = new FunctionListener((entry: ILogEntry) => { // pass all logging data to an existing framework MyExistingCompanyLoggingFramework.log(entry.message); }); Logger.subscribe(listener);","title":"FunctionListener"},{"location":"logging/#create-a-custom-listener","text":"If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface. import { Logger, ILogListener, ILogEntry } from \"@pnp/logging\"; class MyListener implements ILogListener { log(entry: ILogEntry): void { // here you would do something with the entry } } Logger.subscribe(new MyListener());","title":"Create a Custom Listener"},{"location":"news/2020-year-in-review/","text":"2020 Year End Report \u00b6 Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year. This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules. We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community. Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured \"roots\" such as \"sp\" or \"graph\" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios. Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience. Usage \u00b6 In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227. These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November. 1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds Releases \u00b6 We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log , updated with each release. You can check our scheduled releases through project milestones , understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries. NPM Package download statistics (@pnp/sp): \u00b6 Month Count * Month Count January 100,686 * July 36,805 February 34,437 * August 38,897 March 34,574 * September 45,968 April 32,436 * October 46,655 May 34,482 * November 45,511 June 34,408 * December 58,977 Grand Total 543,836 With 2020 our total all time downloads of @pnp/sp is now at: 949,638 Stats from https://npm-stat.com/ Future Plans \u00b6 Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date. Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements. New Lead Maintainer \u00b6 With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner ! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work. Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean. We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come. Contributors \u00b6 As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started. Sponsors \u00b6 We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs. Thank You Closing \u00b6 In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program. Wishing you the very best for 2021, The PnPjs Team","title":"2020 Year In Review"},{"location":"news/2020-year-in-review/#2020-year-end-report","text":"Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year. This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules. We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community. Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured \"roots\" such as \"sp\" or \"graph\" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios. Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.","title":"2020 Year End Report"},{"location":"news/2020-year-in-review/#usage","text":"In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227. These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November. 1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds","title":"Usage"},{"location":"news/2020-year-in-review/#releases","text":"We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log , updated with each release. You can check our scheduled releases through project milestones , understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.","title":"Releases"},{"location":"news/2020-year-in-review/#npm-package-download-statistics-pnpsp","text":"Month Count * Month Count January 100,686 * July 36,805 February 34,437 * August 38,897 March 34,574 * September 45,968 April 32,436 * October 46,655 May 34,482 * November 45,511 June 34,408 * December 58,977 Grand Total 543,836 With 2020 our total all time downloads of @pnp/sp is now at: 949,638 Stats from https://npm-stat.com/","title":"NPM Package download statistics (@pnp/sp):"},{"location":"news/2020-year-in-review/#future-plans","text":"Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date. Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.","title":"Future Plans"},{"location":"news/2020-year-in-review/#new-lead-maintainer","text":"With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner ! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work. Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean. We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.","title":"New Lead Maintainer"},{"location":"news/2020-year-in-review/#contributors","text":"As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.","title":"Contributors"},{"location":"news/2020-year-in-review/#sponsors","text":"We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs. Thank You","title":"Sponsors"},{"location":"news/2020-year-in-review/#closing","text":"In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program. Wishing you the very best for 2021, The PnPjs Team","title":"Closing"},{"location":"nodejs/","text":"@pnp/nodejs \u00b6 This package supplies helper code when using the @pnp libraries within the context of nodejs. Primarily these consist of clients to enable use of the libraries in nodejs. Getting Started \u00b6 Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the exported functionality. npm install @pnp/sp @pnp/nodejs --save AdalFetchClient SPFetchClient BearerTokenFetchClient Proxy SP Extensions \u00b6 Added in 2.0.9 A set of nodejs specific extensions for the @pnp/sp library. SP Extensions","title":"nodejs"},{"location":"nodejs/#pnpnodejs","text":"This package supplies helper code when using the @pnp libraries within the context of nodejs. Primarily these consist of clients to enable use of the libraries in nodejs.","title":"@pnp/nodejs"},{"location":"nodejs/#getting-started","text":"Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the exported functionality. npm install @pnp/sp @pnp/nodejs --save AdalFetchClient SPFetchClient BearerTokenFetchClient Proxy","title":"Getting Started"},{"location":"nodejs/#sp-extensions","text":"Added in 2.0.9 A set of nodejs specific extensions for the @pnp/sp library. SP Extensions","title":"SP Extensions"},{"location":"nodejs/adal-fetch-client/","text":"@pnp/nodejs/adalfetchclient \u00b6 The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"AdalFetchClient"},{"location":"nodejs/adal-fetch-client/#pnpnodejsadalfetchclient","text":"The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"@pnp/nodejs/adalfetchclient"},{"location":"nodejs/bearer-token-fetch-client/","text":"@pnp/nodejs/BearerTokenFetchClient \u00b6 The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. import { BearerTokenFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new BearerTokenFetchClient(\"{Bearer Token}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"BearerTokenFetchClient"},{"location":"nodejs/bearer-token-fetch-client/#pnpnodejsbearertokenfetchclient","text":"The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. import { BearerTokenFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new BearerTokenFetchClient(\"{Bearer Token}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"@pnp/nodejs/BearerTokenFetchClient"},{"location":"nodejs/provider-hosted-app/","text":"@pnp/nodejs/providerhostedrequestcontext \u00b6 The ProviderHostedRequestContext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. Note : To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context. import { sp, SPRest } from \"@pnp/sp/presets/all\"; import { NodeFetchClient, ProviderHostedRequestContext } from \"@pnp/nodejs\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new NodeFetchClient(); }, }, }); // get request data generated by /_layouts/15/AppRedirect.aspx const spAppToken = request.body.SPAppToken; const spSiteUrl = request.body.SPSiteUrl; // create a context based on the add-in details and SPAppToken const ctx = await ProviderHostedRequestContext.create(spSiteUrl, \"{client id}\", \"{client secret}\", spAppToken); // create an SPRest object configured to use our context // this is used in place of the global sp object const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl); const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl); // make a request on behalf of the user const user = await userSP.web.currentUser(); console.log(`Hello ${user.Title}`); // make an add-in only request const app = await addinSP.web.currentUser(); console.log(`Add-in principal: ${app.Title}`);","title":"ProviderHostedRequestContext"},{"location":"nodejs/provider-hosted-app/#pnpnodejsproviderhostedrequestcontext","text":"The ProviderHostedRequestContext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. Note : To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context. import { sp, SPRest } from \"@pnp/sp/presets/all\"; import { NodeFetchClient, ProviderHostedRequestContext } from \"@pnp/nodejs\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new NodeFetchClient(); }, }, }); // get request data generated by /_layouts/15/AppRedirect.aspx const spAppToken = request.body.SPAppToken; const spSiteUrl = request.body.SPSiteUrl; // create a context based on the add-in details and SPAppToken const ctx = await ProviderHostedRequestContext.create(spSiteUrl, \"{client id}\", \"{client secret}\", spAppToken); // create an SPRest object configured to use our context // this is used in place of the global sp object const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl); const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl); // make a request on behalf of the user const user = await userSP.web.currentUser(); console.log(`Hello ${user.Title}`); // make an add-in only request const app = await addinSP.web.currentUser(); console.log(`Add-in principal: ${app.Title}`);","title":"@pnp/nodejs/providerhostedrequestcontext"},{"location":"nodejs/proxy/","text":"@pnp/nodejs/proxy \u00b6 In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler. setProxyUrl \u00b6 Basic Usage \u00b6 You need to import the setProxyUrl function from @pnp/nodejs library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library. import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // call the set proxy url function and it will be used for all requests regardless of client setProxyUrl(\"{your proxy url}\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, }); Use with Fiddler \u00b6 To get Fiddler to work you may need to set an environment variable. This should only be done for testing! import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // ignore certificate errors: ONLY FOR TESTING!! process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\"; // this is my fiddler url locally setProxyUrl(\"http://127.0.0.1:8888\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, }); setProxyAgent \u00b6 Added in 2.0.11 You need to import the setProxyAgent function from @pnp/nodejs library and call it with your proxy url. You can supply any valid proxy and it will be used. import { SPFetchClient, SPOAuthEnv, setProxyAgent } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { const myAgent = new MyAgentOfSomeType({}); // call the set proxy agent function and it will be used for all requests regardless of client setProxyAgent(myAgent); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"proxy"},{"location":"nodejs/proxy/#pnpnodejsproxy","text":"In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler.","title":"@pnp/nodejs/proxy"},{"location":"nodejs/proxy/#setproxyurl","text":"","title":"setProxyUrl"},{"location":"nodejs/proxy/#basic-usage","text":"You need to import the setProxyUrl function from @pnp/nodejs library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library. import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // call the set proxy url function and it will be used for all requests regardless of client setProxyUrl(\"{your proxy url}\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"Basic Usage"},{"location":"nodejs/proxy/#use-with-fiddler","text":"To get Fiddler to work you may need to set an environment variable. This should only be done for testing! import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // ignore certificate errors: ONLY FOR TESTING!! process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\"; // this is my fiddler url locally setProxyUrl(\"http://127.0.0.1:8888\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"Use with Fiddler"},{"location":"nodejs/proxy/#setproxyagent","text":"Added in 2.0.11 You need to import the setProxyAgent function from @pnp/nodejs library and call it with your proxy url. You can supply any valid proxy and it will be used. import { SPFetchClient, SPOAuthEnv, setProxyAgent } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { const myAgent = new MyAgentOfSomeType({}); // call the set proxy agent function and it will be used for all requests regardless of client setProxyAgent(myAgent); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"setProxyAgent"},{"location":"nodejs/sp-extensions/","text":"@pnp/nodejs - sp extensions \u00b6 By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api. This article describes them. These examples use the *-commonjs version of the libraries as they target node, you can read more about the differences . IFile.getStream \u00b6 Allows you to read a response body as a nodejs PassThrough stream. // by importing the the library the node specific extensions are automatically applied import { SPFetchClient, SPNS } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{url}\", \"{id}\", \"{secret}\"); }, }, }); // get the stream const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream(); // see if we have a known length console.log(streamResult.knownLength); // read the stream // this is a very basic example - you can do tons more with streams in node const txt = await new Promise((resolve) => { let data = \"\"; stream.body.on(\"data\", (chunk) => data += chunk); stream.body.on(\"end\", () => resolve(data)); }); IFiles.addChunked \u00b6 Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const files = sp.web.defaultDocumentLibrary.rootFolder.files; await files.addChunked(name, stream, null, true, 10); IFile.setStreamContentChunked \u00b6 Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName(\"file-name.txt\"); await file.setStreamContentChunked(stream); Explicit import \u00b6 If you don't need to import anything from the library, but would like to include the extensions just import the library as shown. // ES Modules: import \"@pnp/nodejs\"; import \"@pnp/nodejs-commonjs\"; // get the stream const streamResult = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream(); Accessing SP Extension Namespace \u00b6 There are classes and interfaces included in extension modules, which you can access through a namespace, \"SPNS\". import { SPNS } from \"@pnp/nodejs-commonjs\"; const parser = new SPNS.StreamParser();","title":"sp Extensions"},{"location":"nodejs/sp-extensions/#pnpnodejs-sp-extensions","text":"By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api. This article describes them. These examples use the *-commonjs version of the libraries as they target node, you can read more about the differences .","title":"@pnp/nodejs - sp extensions"},{"location":"nodejs/sp-extensions/#ifilegetstream","text":"Allows you to read a response body as a nodejs PassThrough stream. // by importing the the library the node specific extensions are automatically applied import { SPFetchClient, SPNS } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{url}\", \"{id}\", \"{secret}\"); }, }, }); // get the stream const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream(); // see if we have a known length console.log(streamResult.knownLength); // read the stream // this is a very basic example - you can do tons more with streams in node const txt = await new Promise((resolve) => { let data = \"\"; stream.body.on(\"data\", (chunk) => data += chunk); stream.body.on(\"end\", () => resolve(data)); });","title":"IFile.getStream"},{"location":"nodejs/sp-extensions/#ifilesaddchunked","text":"Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const files = sp.web.defaultDocumentLibrary.rootFolder.files; await files.addChunked(name, stream, null, true, 10);","title":"IFiles.addChunked"},{"location":"nodejs/sp-extensions/#ifilesetstreamcontentchunked","text":"Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName(\"file-name.txt\"); await file.setStreamContentChunked(stream);","title":"IFile.setStreamContentChunked"},{"location":"nodejs/sp-extensions/#explicit-import","text":"If you don't need to import anything from the library, but would like to include the extensions just import the library as shown. // ES Modules: import \"@pnp/nodejs\"; import \"@pnp/nodejs-commonjs\"; // get the stream const streamResult = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream();","title":"Explicit import"},{"location":"nodejs/sp-extensions/#accessing-sp-extension-namespace","text":"There are classes and interfaces included in extension modules, which you can access through a namespace, \"SPNS\". import { SPNS } from \"@pnp/nodejs-commonjs\"; const parser = new SPNS.StreamParser();","title":"Accessing SP Extension Namespace"},{"location":"nodejs/sp-fetch-client/","text":"@pnp/nodejs/spfetchclient \u00b6 The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. See: How to register a legacy SharePoint application import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web(); console.log(JSON.stringify(w, null, 4)); Set Authentication Environment \u00b6 For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. SPO : (default) for all *.sharepoint.com urls China: for China hosted cloud Germany: for Germany local cloud USDef: USA Defense cloud USGov: USA Government cloud import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.China); }, }, }); Set Realm \u00b6 In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx and copying the GUID value that appears after the \"@\" - this is the realm id. import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.SPO, \"{realm}\"); }, }, });","title":"SPFetchClient"},{"location":"nodejs/sp-fetch-client/#pnpnodejsspfetchclient","text":"The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. See: How to register a legacy SharePoint application import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web(); console.log(JSON.stringify(w, null, 4));","title":"@pnp/nodejs/spfetchclient"},{"location":"nodejs/sp-fetch-client/#set-authentication-environment","text":"For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. SPO : (default) for all *.sharepoint.com urls China: for China hosted cloud Germany: for Germany local cloud USDef: USA Defense cloud USGov: USA Government cloud import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.China); }, }, });","title":"Set Authentication Environment"},{"location":"nodejs/sp-fetch-client/#set-realm","text":"In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx and copying the GUID value that appears after the \"@\" - this is the realm id. import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.SPO, \"{realm}\"); }, }, });","title":"Set Realm"},{"location":"odata/","text":"@pnp/queryable \u00b6 This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure the core code is solid and well tested, with any updates benefitting all inheriting libraries. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable --save Library Topics \u00b6 caching core OData Batching Parsers Pipeline Queryable","title":"odata"},{"location":"odata/#pnpqueryable","text":"This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure the core code is solid and well tested, with any updates benefitting all inheriting libraries.","title":"@pnp/queryable"},{"location":"odata/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable --save","title":"Getting Started"},{"location":"odata/#library-topics","text":"caching core OData Batching Parsers Pipeline Queryable","title":"Library Topics"},{"location":"odata/caching/","text":"@pnp/queryable/caching \u00b6 Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph. Basic example \u00b6 You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The code below will get items from a list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() method should always be the last method in the chain before the get() (OR if you are using batching these methods can be transposed, more details below). import { sp } from \"@pnp/sp\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.usingCaching()(); console.log(r); const r2 = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r2); Globally Configure Cache Settings \u00b6 If you would not like to use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. import { sp } from \"@pnp/sp\"; sp.setup({ defaultCachingStore: \"session\", // or \"local\" defaultCachingTimeoutSeconds: 30, globalCacheDisable: false // or true to disable caching in case of debugging/testing }); const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r); Per Call Configuration \u00b6 If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. export interface ICachingOptions { expiration?: Date; storeName?: \"session\" | \"local\"; key: string; } import { sp } from \"@pnp/sp\"; import { dateAdd } from \"@pnp/core\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching({ expiration: dateAdd(new Date(), \"minute\", 20), key: \"My Key\", storeName: \"local\" })(); console.log(r); Using Batching with Caching \u00b6 You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. import { sp } from \"@pnp/sp\"; let batch = sp.createBatch(); sp.web.lists.inBatch(batch).usingCaching()().then(r => { console.log(r) }); sp.web.lists.getByTitle(\"Tasks\").items.usingCaching().inBatch(batch)().then(r => { console.log(r) }); batch.execute().then(() => console.log(\"All done!\")); Implement Custom Caching \u00b6 You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here . Implement caching helper method \u00b6 We create a map to act as our cache storage and a function to wrap the request caching logic const map = new Map(); async function staleWhileRevalidate(key: string, p: Promise): Promise { if (map.has(key)) { // In Cache p.then(u => { // Update Cache once we have a result map.set(key, u); }); // Return from Cache return map.get(key); } // Not In Cache so we need to wait for the value const r = await p; // Set Cache map.set(key, r); // Return from Promise return r; } Usage \u00b6 Don't call usingCaching just apply the helper method // this one will wait for the request to finish const r1 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r2, null, 2)); Wrapper Function \u00b6 You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. interface WebData { Title: string; Description: string; } function getWebData(): Promise { return staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); } // this one will wait for the request to finish const r1 = await getWebData(); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await getWebData(); console.log(JSON.stringify(r2, null, 2));","title":"caching"},{"location":"odata/caching/#pnpqueryablecaching","text":"Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph.","title":"@pnp/queryable/caching"},{"location":"odata/caching/#basic-example","text":"You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The code below will get items from a list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() method should always be the last method in the chain before the get() (OR if you are using batching these methods can be transposed, more details below). import { sp } from \"@pnp/sp\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.usingCaching()(); console.log(r); const r2 = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r2);","title":"Basic example"},{"location":"odata/caching/#globally-configure-cache-settings","text":"If you would not like to use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. import { sp } from \"@pnp/sp\"; sp.setup({ defaultCachingStore: \"session\", // or \"local\" defaultCachingTimeoutSeconds: 30, globalCacheDisable: false // or true to disable caching in case of debugging/testing }); const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r);","title":"Globally Configure Cache Settings"},{"location":"odata/caching/#per-call-configuration","text":"If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. export interface ICachingOptions { expiration?: Date; storeName?: \"session\" | \"local\"; key: string; } import { sp } from \"@pnp/sp\"; import { dateAdd } from \"@pnp/core\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching({ expiration: dateAdd(new Date(), \"minute\", 20), key: \"My Key\", storeName: \"local\" })(); console.log(r);","title":"Per Call Configuration"},{"location":"odata/caching/#using-batching-with-caching","text":"You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. import { sp } from \"@pnp/sp\"; let batch = sp.createBatch(); sp.web.lists.inBatch(batch).usingCaching()().then(r => { console.log(r) }); sp.web.lists.getByTitle(\"Tasks\").items.usingCaching().inBatch(batch)().then(r => { console.log(r) }); batch.execute().then(() => console.log(\"All done!\"));","title":"Using Batching with Caching"},{"location":"odata/caching/#implement-custom-caching","text":"You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here .","title":"Implement Custom Caching"},{"location":"odata/caching/#implement-caching-helper-method","text":"We create a map to act as our cache storage and a function to wrap the request caching logic const map = new Map(); async function staleWhileRevalidate(key: string, p: Promise): Promise { if (map.has(key)) { // In Cache p.then(u => { // Update Cache once we have a result map.set(key, u); }); // Return from Cache return map.get(key); } // Not In Cache so we need to wait for the value const r = await p; // Set Cache map.set(key, r); // Return from Promise return r; }","title":"Implement caching helper method"},{"location":"odata/caching/#usage","text":"Don't call usingCaching just apply the helper method // this one will wait for the request to finish const r1 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r2, null, 2));","title":"Usage"},{"location":"odata/caching/#wrapper-function","text":"You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. interface WebData { Title: string; Description: string; } function getWebData(): Promise { return staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); } // this one will wait for the request to finish const r1 = await getWebData(); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await getWebData(); console.log(JSON.stringify(r2, null, 2));","title":"Wrapper Function"},{"location":"odata/core/","text":"@pnp/queryable/core \u00b6 This module contains shared interfaces and abstract classes used within the @pnp/queryable package and those items that inherit from it. ProcessHttpClientResponseException \u00b6 The exception thrown when a response is returned and cannot be processed. interface ODataParser \u00b6 Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the value to be returned. It has two methods, one is optional: parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor ODataParserBase \u00b6 The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods. Create a custom parser from ODataParserBase \u00b6 You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases. class MyParser extends ODataParserBase { // we need to override the parse method to do our custom stuff public parse(r: Response): Promise { // we wrap everything in a promise return new Promise((resolve, reject) => { // lets use the default error handling which returns true for no error // and will call reject with an error if one exists if (this.handleError(r, reject)) { // now we add our custom parsing here r.text().then(txt => { // here we call a made up function to parse the result // this is where we would do our parsing as required myCustomerUnencode(txt).then(v => { resolve(v); }); }); } }); } }","title":"core"},{"location":"odata/core/#pnpqueryablecore","text":"This module contains shared interfaces and abstract classes used within the @pnp/queryable package and those items that inherit from it.","title":"@pnp/queryable/core"},{"location":"odata/core/#processhttpclientresponseexception","text":"The exception thrown when a response is returned and cannot be processed.","title":"ProcessHttpClientResponseException"},{"location":"odata/core/#interface-odataparser","text":"Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the value to be returned. It has two methods, one is optional: parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor","title":"interface ODataParser"},{"location":"odata/core/#odataparserbase","text":"The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods.","title":"ODataParserBase"},{"location":"odata/core/#create-a-custom-parser-from-odataparserbase","text":"You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases. class MyParser extends ODataParserBase { // we need to override the parse method to do our custom stuff public parse(r: Response): Promise { // we wrap everything in a promise return new Promise((resolve, reject) => { // lets use the default error handling which returns true for no error // and will call reject with an error if one exists if (this.handleError(r, reject)) { // now we add our custom parsing here r.text().then(txt => { // here we call a made up function to parse the result // this is where we would do our parsing as required myCustomerUnencode(txt).then(v => { resolve(v); }); }); } }); } }","title":"Create a custom parser from ODataParserBase"},{"location":"odata/debug/","text":"Debugging Proxy Objects \u00b6 Because all queryables are now represented as Proxy objects you can't immediately see the properties/method of the object or the data stored about the request. In certain debugging scenarios it can help to get visibility into the object that is wrapped by the proxy. To enable this we provide a set of extensions to help. The debug extensions are added by including the import \"@pnp/queryable/debug\"; statement in your project. It should be removed for production. This module provides several methods to help with debugging Queryable Proxy objects. Unwrap \u00b6 The __unwrap() method returns the concrete Queryable instance wrapped by the Proxy. You can then examine this object in various ways or dump it to the console for debugging. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // unwrap the underlying concrete queryable instance const unwrapped = sp.web.__unwrap(); console.log(JSON.stringify(unwrapped, null, 2)); Note: It is not supported to unwrap objects and then use them. It may work in some cases, but this behavior may change as what is contained with the Proxy is an implementation detail and should not be relied upon. Without the Proxy wrapper we make no guarantees. Data \u00b6 All of the information related to a queryable's request is contained within the \"data\" property. If you need to grab that information you can use the __data property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's data const data = sp.web.__data; console.log(JSON.stringify(data, null, 2)); JSON \u00b6 You can also get a representation of the wrapped instance in JSON format consisting of all its own properties and values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's as JSON const data = sp.web.__json(); console.log(JSON.stringify(data, null, 2)); Deep Trace \u00b6 Deep tracing is the ability to write every property and method access to the log. This produces VERY verbose output but can be helpful in situations where you need to trace how things are called and when within the Proxy. You enable deep tracing using the __enableDeepTrace method and disable using __disableDeepTrace . import { Logger, ConsoleListener } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; Logger.subscribe(new ConsoleListener()); // grab an instance to enable deep trace const web = sp.web; // enable deep trace on the instance web.__enableDeepTrace(); const y = await web.lists(); // disable deep trace web.__disableDeepTrace(); The example above produces the following output: Message: get ::> lists Message: get ::> lists Message: get ::> toUrl Message: get ::> toUrl Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> query Message: get ::> query Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: get ::> __disableDeepTrace Message: get ::> __disableDeepTrace","title":"Debugging Proxy Objects"},{"location":"odata/debug/#debugging-proxy-objects","text":"Because all queryables are now represented as Proxy objects you can't immediately see the properties/method of the object or the data stored about the request. In certain debugging scenarios it can help to get visibility into the object that is wrapped by the proxy. To enable this we provide a set of extensions to help. The debug extensions are added by including the import \"@pnp/queryable/debug\"; statement in your project. It should be removed for production. This module provides several methods to help with debugging Queryable Proxy objects.","title":"Debugging Proxy Objects"},{"location":"odata/debug/#unwrap","text":"The __unwrap() method returns the concrete Queryable instance wrapped by the Proxy. You can then examine this object in various ways or dump it to the console for debugging. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // unwrap the underlying concrete queryable instance const unwrapped = sp.web.__unwrap(); console.log(JSON.stringify(unwrapped, null, 2)); Note: It is not supported to unwrap objects and then use them. It may work in some cases, but this behavior may change as what is contained with the Proxy is an implementation detail and should not be relied upon. Without the Proxy wrapper we make no guarantees.","title":"Unwrap"},{"location":"odata/debug/#data","text":"All of the information related to a queryable's request is contained within the \"data\" property. If you need to grab that information you can use the __data property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's data const data = sp.web.__data; console.log(JSON.stringify(data, null, 2));","title":"Data"},{"location":"odata/debug/#json","text":"You can also get a representation of the wrapped instance in JSON format consisting of all its own properties and values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's as JSON const data = sp.web.__json(); console.log(JSON.stringify(data, null, 2));","title":"JSON"},{"location":"odata/debug/#deep-trace","text":"Deep tracing is the ability to write every property and method access to the log. This produces VERY verbose output but can be helpful in situations where you need to trace how things are called and when within the Proxy. You enable deep tracing using the __enableDeepTrace method and disable using __disableDeepTrace . import { Logger, ConsoleListener } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; Logger.subscribe(new ConsoleListener()); // grab an instance to enable deep trace const web = sp.web; // enable deep trace on the instance web.__enableDeepTrace(); const y = await web.lists(); // disable deep trace web.__disableDeepTrace(); The example above produces the following output: Message: get ::> lists Message: get ::> lists Message: get ::> toUrl Message: get ::> toUrl Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> query Message: get ::> query Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: get ::> __disableDeepTrace Message: get ::> __disableDeepTrace","title":"Deep Trace"},{"location":"odata/extensions/","text":"Extensions \u00b6 introduced in 2.0.0 Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invocable . You can control any behavior of the library with extensions. Extensions do not work in ie11 compatibility mode. This is by design. Types of Extensions \u00b6 There are three types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options. Function Extensions \u00b6 The first type is a simple function with a signature: (op: \"apply\" | \"get\" | \"has\" | \"set\", target: T, ...rest: any[]): void This function is passed the current operation as the first argument, currently one of \"apply\", \"get\", \"has\", or \"set\". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures. Named Extensions \u00b6 Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables. import { extendFactory } from \"@pnp/queryable\"; import { sp, List, Lists, IWeb, ILists, List, IList, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeQueryStrValue\"; // create a plain object with the props and methods we want to add/change const myExtensions = { // override the lists property get lists(this: IWeb): ILists { // we will always order our lists by title and select just the Title for ALL calls (just as an example) return Lists(this).orderBy(\"Title\").select(\"Title\"); }, // override the getByTitle method getByTitle: function (this: ILists, title: string): IList { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, `getByTitle('List2')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; // register all the named Extensions extendFactory(Web, myExtensions); // this will use our extension to ensure the lists are ordered const lists = await sp.web.lists(); console.log(JSON.stringify(lists, null, 2)); // we will get the items from List1 but within the extension it is rewritten as List2 const items = await sp.web.lists.getByTitle(\"List1\").items(); console.log(JSON.stringify(items.length, null, 2)); ProxyHandler Extensions \u00b6 You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work. import { extendFactory } from \"@pnp/queryable\"; import { sp, Lists, IWeb, ILists, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeSingleQuote\"; const myExtensions = { get: (target, p: string | number | symbol, _receiver: any) => { switch (p) { case \"getByTitle\": return (title: string) => { // in our example our list has moved, so we rewrite the request on the fly if (title === \"LookupList\") { return List(target, `getByTitle('OrderByList')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(target, `getByTitle('${escapeQueryStrValue(title)}')`); } }; } }, }; extendFactory(Web, myExtensions); const lists = sp.web.lists; const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2)); Registering Extensions \u00b6 You can register Extensions either globally, on an invocable factory, or on a per-object basis, and you can register a single extension or an array of Extensions. Global Registration \u00b6 Globally registering an extension allows you to inject functionality into every invocable that is instantiated within your application. It is important to remember that processing extensions happens on ALL property access and method invocation operations - so global extensions should be used sparingly. import { extendGlobal } from \"@pnp/queryable\"; // we can add a logging method to very verbosely track what things are called in our application extendGlobal((op: string, _target: any, ...rest: any[]): void => { switch (op) { case \"apply\": Logger.write(`${op} ::> ()`, LogLevel.Info); break; case \"has\": case \"get\": case \"set\": Logger.write(`${op} ::> ${rest[0]}`, LogLevel.Info); break; default: Logger.write(`unknown ${op}`, LogLevel.Info); } }); Factory Registration \u00b6 The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList. import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import { IWeb, Web } from \"@pnp/sp/webs\"; import { ILists, Lists } from \"@pnp/sp/lists\"; import { extendFactory } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // sets up the types correctly when importing across your application declare module \"@pnp/sp/webs/types\" { // we need to extend the interface interface IWeb { orderedLists: ILists; } } // sets up the types correctly when importing across your application declare module \"@pnp/sp/lists/types\" { // we need to extend the interface interface ILists { getOrderedListsQuery: (this: ILists) => ILists; } } extendFactory(Web, { // add an ordered lists property get orderedLists(this: IWeb): ILists { return this.lists.getOrderedListsQuery(); }, }); extendFactory(Lists, { // add an ordered lists property getOrderedListsQuery(this: ILists): ILists { return this.top(10).orderBy(\"Title\").select(\"Title\"); }, }); // regardless of how we access the web and lists collections our extensions remain with all new instance based on const web = Web(\"https://tenant.sharepoint.com/sites/dev/\"); const lists1 = await web.orderedLists(); console.log(JSON.stringify(lists1, null, 2)); const lists2 = await Web(\"https://tenant.sharepoint.com/sites/dev/\").orderedLists(); console.log(JSON.stringify(lists2, null, 2)); const lists3 = await sp.web.orderedLists(); console.log(JSON.stringify(lists3, null, 2)); Instance Registration \u00b6 You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances. Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are. Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance. import { extendObj } from \"@pnp/queryable\"; import { sp, List, ILists } from \"@pnp/sp/presets/all\"; const myExtensions = { getByTitle: function (this: ILists, title: string) { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, \"getByTitle('List2')\"); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; const lists = extendObj(sp.web.lists, myExtensions); const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2)); Enable & Disable Extensions and Clear Global Extensions \u00b6 Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed. import { enableExtensions, disableExtensions, clearGlobalExtensions } from \"@pnp/queryable\"; // disable Extensions disableExtensions(); // enable Extensions enableExtensions(); // clear all the globally registered extensions clearGlobalExtensions(); Order of Operations \u00b6 It is important to understand the order in which extensions are executed and when a value is returned. Instance extensions* are always called first, followed by global Extensions - in both cases they are called in the order they were registered. This allows you to perhaps have some global functionality while maintaining the ability to override it again at the instance level. IF an extension returns a value other than undefined that value is returned and no other extensions are processed. *extensions applied via an extended factory are considered instance extensions","title":"Extending an OData library"},{"location":"odata/extensions/#extensions","text":"introduced in 2.0.0 Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invocable . You can control any behavior of the library with extensions. Extensions do not work in ie11 compatibility mode. This is by design.","title":"Extensions"},{"location":"odata/extensions/#types-of-extensions","text":"There are three types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.","title":"Types of Extensions"},{"location":"odata/extensions/#function-extensions","text":"The first type is a simple function with a signature: (op: \"apply\" | \"get\" | \"has\" | \"set\", target: T, ...rest: any[]): void This function is passed the current operation as the first argument, currently one of \"apply\", \"get\", \"has\", or \"set\". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.","title":"Function Extensions"},{"location":"odata/extensions/#named-extensions","text":"Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables. import { extendFactory } from \"@pnp/queryable\"; import { sp, List, Lists, IWeb, ILists, List, IList, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeQueryStrValue\"; // create a plain object with the props and methods we want to add/change const myExtensions = { // override the lists property get lists(this: IWeb): ILists { // we will always order our lists by title and select just the Title for ALL calls (just as an example) return Lists(this).orderBy(\"Title\").select(\"Title\"); }, // override the getByTitle method getByTitle: function (this: ILists, title: string): IList { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, `getByTitle('List2')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; // register all the named Extensions extendFactory(Web, myExtensions); // this will use our extension to ensure the lists are ordered const lists = await sp.web.lists(); console.log(JSON.stringify(lists, null, 2)); // we will get the items from List1 but within the extension it is rewritten as List2 const items = await sp.web.lists.getByTitle(\"List1\").items(); console.log(JSON.stringify(items.length, null, 2));","title":"Named Extensions"},{"location":"odata/extensions/#proxyhandler-extensions","text":"You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work. import { extendFactory } from \"@pnp/queryable\"; import { sp, Lists, IWeb, ILists, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeSingleQuote\"; const myExtensions = { get: (target, p: string | number | symbol, _receiver: any) => { switch (p) { case \"getByTitle\": return (title: string) => { // in our example our list has moved, so we rewrite the request on the fly if (title === \"LookupList\") { return List(target, `getByTitle('OrderByList')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(target, `getByTitle('${escapeQueryStrValue(title)}')`); } }; } }, }; extendFactory(Web, myExtensions); const lists = sp.web.lists; const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2));","title":"ProxyHandler Extensions"},{"location":"odata/extensions/#registering-extensions","text":"You can register Extensions either globally, on an invocable factory, or on a per-object basis, and you can register a single extension or an array of Extensions.","title":"Registering Extensions"},{"location":"odata/extensions/#global-registration","text":"Globally registering an extension allows you to inject functionality into every invocable that is instantiated within your application. It is important to remember that processing extensions happens on ALL property access and method invocation operations - so global extensions should be used sparingly. import { extendGlobal } from \"@pnp/queryable\"; // we can add a logging method to very verbosely track what things are called in our application extendGlobal((op: string, _target: any, ...rest: any[]): void => { switch (op) { case \"apply\": Logger.write(`${op} ::> ()`, LogLevel.Info); break; case \"has\": case \"get\": case \"set\": Logger.write(`${op} ::> ${rest[0]}`, LogLevel.Info); break; default: Logger.write(`unknown ${op}`, LogLevel.Info); } });","title":"Global Registration"},{"location":"odata/extensions/#factory-registration","text":"The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList. import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import { IWeb, Web } from \"@pnp/sp/webs\"; import { ILists, Lists } from \"@pnp/sp/lists\"; import { extendFactory } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // sets up the types correctly when importing across your application declare module \"@pnp/sp/webs/types\" { // we need to extend the interface interface IWeb { orderedLists: ILists; } } // sets up the types correctly when importing across your application declare module \"@pnp/sp/lists/types\" { // we need to extend the interface interface ILists { getOrderedListsQuery: (this: ILists) => ILists; } } extendFactory(Web, { // add an ordered lists property get orderedLists(this: IWeb): ILists { return this.lists.getOrderedListsQuery(); }, }); extendFactory(Lists, { // add an ordered lists property getOrderedListsQuery(this: ILists): ILists { return this.top(10).orderBy(\"Title\").select(\"Title\"); }, }); // regardless of how we access the web and lists collections our extensions remain with all new instance based on const web = Web(\"https://tenant.sharepoint.com/sites/dev/\"); const lists1 = await web.orderedLists(); console.log(JSON.stringify(lists1, null, 2)); const lists2 = await Web(\"https://tenant.sharepoint.com/sites/dev/\").orderedLists(); console.log(JSON.stringify(lists2, null, 2)); const lists3 = await sp.web.orderedLists(); console.log(JSON.stringify(lists3, null, 2));","title":"Factory Registration"},{"location":"odata/extensions/#instance-registration","text":"You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances. Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are. Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance. import { extendObj } from \"@pnp/queryable\"; import { sp, List, ILists } from \"@pnp/sp/presets/all\"; const myExtensions = { getByTitle: function (this: ILists, title: string) { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, \"getByTitle('List2')\"); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; const lists = extendObj(sp.web.lists, myExtensions); const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2));","title":"Instance Registration"},{"location":"odata/extensions/#enable-disable-extensions-and-clear-global-extensions","text":"Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed. import { enableExtensions, disableExtensions, clearGlobalExtensions } from \"@pnp/queryable\"; // disable Extensions disableExtensions(); // enable Extensions enableExtensions(); // clear all the globally registered extensions clearGlobalExtensions();","title":"Enable & Disable Extensions and Clear Global Extensions"},{"location":"odata/extensions/#order-of-operations","text":"It is important to understand the order in which extensions are executed and when a value is returned. Instance extensions* are always called first, followed by global Extensions - in both cases they are called in the order they were registered. This allows you to perhaps have some global functionality while maintaining the ability to override it again at the instance level. IF an extension returns a value other than undefined that value is returned and no other extensions are processed. *extensions applied via an extended factory are considered instance extensions","title":"Order of Operations"},{"location":"odata/odata-batch/","text":"@pnp/queryable/odatabatch \u00b6 This module contains an abstract class used as a base when inheriting libraries support batching. ODataBatchRequestInfo \u00b6 This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will be responsible for processing that info by implementing the abstract executeImpl method. ODataBatch \u00b6 Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp and @pnp/graph modules.","title":"OData Batching"},{"location":"odata/odata-batch/#pnpqueryableodatabatch","text":"This module contains an abstract class used as a base when inheriting libraries support batching.","title":"@pnp/queryable/odatabatch"},{"location":"odata/odata-batch/#odatabatchrequestinfo","text":"This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will be responsible for processing that info by implementing the abstract executeImpl method.","title":"ODataBatchRequestInfo"},{"location":"odata/odata-batch/#odatabatch","text":"Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp and @pnp/graph modules.","title":"ODataBatch"},{"location":"odata/parsers/","text":"@pnp/queryable/parsers \u00b6 This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need. ODataDefaultParser \u00b6 The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. import { sp } from \"@pnp/sp\"; import { JSONParser } from \"@pnp/queryable\"; try { const parser = new JSONParser(); // this always throws a 404 error await sp.web.getList(\"doesn't exist\").get(parser); } catch (e) { // we can check for the property \"isHttpRequestError\" to see if this is an instance of our class // this gets by all the many limitations of subclassing Error and type detection in JavaScript if (e.hasOwnProperty(\"isHttpRequestError\")) { console.log(\"e is HttpRequestError\"); // now we can access the various properties and make use of the response object. // at this point the body is unread console.log(`status: ${e.status}`); console.log(`statusText: ${e.statusText}`); const json = await e.response.clone().json(); console.log(JSON.stringify(json)); const text = await e.response.clone().text(); console.log(text); const headers = e.response.headers; } console.error(e); } TextParser \u00b6 Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files. BlobParser \u00b6 Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files. JSONParser \u00b6 Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files. BufferParser \u00b6 Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files. LambdaParser \u00b6 Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type. import { LambdaParser } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // here a simple parser duplicating the functionality of the JSONParser const parser = new LambdaParser((r: Response) => r.json()); const webDataJson = await sp.web.get(parser); console.log(webDataJson);","title":"Parsers"},{"location":"odata/parsers/#pnpqueryableparsers","text":"This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need.","title":"@pnp/queryable/parsers"},{"location":"odata/parsers/#odatadefaultparser","text":"The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. import { sp } from \"@pnp/sp\"; import { JSONParser } from \"@pnp/queryable\"; try { const parser = new JSONParser(); // this always throws a 404 error await sp.web.getList(\"doesn't exist\").get(parser); } catch (e) { // we can check for the property \"isHttpRequestError\" to see if this is an instance of our class // this gets by all the many limitations of subclassing Error and type detection in JavaScript if (e.hasOwnProperty(\"isHttpRequestError\")) { console.log(\"e is HttpRequestError\"); // now we can access the various properties and make use of the response object. // at this point the body is unread console.log(`status: ${e.status}`); console.log(`statusText: ${e.statusText}`); const json = await e.response.clone().json(); console.log(JSON.stringify(json)); const text = await e.response.clone().text(); console.log(text); const headers = e.response.headers; } console.error(e); }","title":"ODataDefaultParser"},{"location":"odata/parsers/#textparser","text":"Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files.","title":"TextParser"},{"location":"odata/parsers/#blobparser","text":"Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files.","title":"BlobParser"},{"location":"odata/parsers/#jsonparser","text":"Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files.","title":"JSONParser"},{"location":"odata/parsers/#bufferparser","text":"Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files.","title":"BufferParser"},{"location":"odata/parsers/#lambdaparser","text":"Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type. import { LambdaParser } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // here a simple parser duplicating the functionality of the JSONParser const parser = new LambdaParser((r: Response) => r.json()); const webDataJson = await sp.web.get(parser); console.log(webDataJson);","title":"LambdaParser"},{"location":"odata/pipeline/","text":"@pnp/queryable/pipeline \u00b6 All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline. interface RequestContext \u00b6 The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required. interface RequestContext { batch: ODataBatch; batchDependency: () => void; cachingOptions: ICachingOptions; hasResult?: boolean; isBatched: boolean; isCached: boolean; options: FetchOptions; parser: ODataParser; pipeline: Array<(c: RequestContext) => Promise>>; requestAbsoluteUrl: string; requestId: string; result?: T; verb: string; clientFactory: () => RequestClient; } requestPipelineMethod decorator \u00b6 The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existence of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. @requestPipelineMethod(true) public static myPipelineMethod(context: RequestContext): Promise> { return new Promise>(resolve => { // do something resolve(context); }); } Default Pipeline \u00b6 logs the start of the request checks the cache for a value based on the context's cache settings sends the request if no value from found in the cache logs the end of the request","title":"Pipeline"},{"location":"odata/pipeline/#pnpqueryablepipeline","text":"All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline.","title":"@pnp/queryable/pipeline"},{"location":"odata/pipeline/#interface-requestcontextt","text":"The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required. interface RequestContext { batch: ODataBatch; batchDependency: () => void; cachingOptions: ICachingOptions; hasResult?: boolean; isBatched: boolean; isCached: boolean; options: FetchOptions; parser: ODataParser; pipeline: Array<(c: RequestContext) => Promise>>; requestAbsoluteUrl: string; requestId: string; result?: T; verb: string; clientFactory: () => RequestClient; }","title":"interface RequestContext<T>"},{"location":"odata/pipeline/#requestpipelinemethod-decorator","text":"The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existence of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. @requestPipelineMethod(true) public static myPipelineMethod(context: RequestContext): Promise> { return new Promise>(resolve => { // do something resolve(context); }); }","title":"requestPipelineMethod decorator"},{"location":"odata/pipeline/#default-pipeline","text":"logs the start of the request checks the cache for a value based on the context's cache settings sends the request if no value from found in the cache logs the end of the request","title":"Default Pipeline"},{"location":"odata/queryable/","text":"@pnp/queryable/queryable \u00b6 The Queryable class is the base class for all of the libraries building fluent request apis. abstract class ODataQueryable \u00b6 This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls. Properties \u00b6 query \u00b6 Provides access to the query string builder for this url Public Methods \u00b6 concat \u00b6 Directly concatenates the supplied string to the current url, not normalizing \"/\" chars configure \u00b6 Sets custom options for current object and all derived objects accessible via chaining import { ConfigOptions } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; const headers: ConfigOptions = { Accept: 'application/json;odata=nometadata' }; // here we use configure to set the headers value for all child requests of the list instance const list = sp.web.lists.getByTitle(\"List1\").configure({ headers }); // this will use the values set in configure list.items().then(items => console.log(JSON.stringify(items, null, 2)); For reference the ConfigOptions interface is shown below: export interface ConfigOptions { headers?: string[][] | { [key: string]: string } | Headers; mode?: \"navigate\" | \"same-origin\" | \"no-cors\" | \"cors\"; credentials?: \"omit\" | \"same-origin\" | \"include\"; cache?: \"default\" | \"no-store\" | \"reload\" | \"no-cache\" | \"force-cache\" | \"only-if-cached\"; } configureFrom \u00b6 Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance. usingCaching \u00b6 Enables caching for this request. See caching for more details. import { sp } from \"@pnp/sp\" sp.web.usingCaching()().then(...); inBatch \u00b6 Adds this query to the supplied batch toUrl \u00b6 Gets the current url abstract toUrlAndQuery() \u00b6 When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request get \u00b6 Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.","title":"Queryable"},{"location":"odata/queryable/#pnpqueryablequeryable","text":"The Queryable class is the base class for all of the libraries building fluent request apis.","title":"@pnp/queryable/queryable"},{"location":"odata/queryable/#abstract-class-odataqueryablebatchtype-extends-odatabatch","text":"This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls.","title":"abstract class ODataQueryable<BatchType extends ODataBatch>"},{"location":"odata/queryable/#properties","text":"","title":"Properties"},{"location":"odata/queryable/#query","text":"Provides access to the query string builder for this url","title":"query"},{"location":"odata/queryable/#public-methods","text":"","title":"Public Methods"},{"location":"odata/queryable/#concat","text":"Directly concatenates the supplied string to the current url, not normalizing \"/\" chars","title":"concat"},{"location":"odata/queryable/#configure","text":"Sets custom options for current object and all derived objects accessible via chaining import { ConfigOptions } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; const headers: ConfigOptions = { Accept: 'application/json;odata=nometadata' }; // here we use configure to set the headers value for all child requests of the list instance const list = sp.web.lists.getByTitle(\"List1\").configure({ headers }); // this will use the values set in configure list.items().then(items => console.log(JSON.stringify(items, null, 2)); For reference the ConfigOptions interface is shown below: export interface ConfigOptions { headers?: string[][] | { [key: string]: string } | Headers; mode?: \"navigate\" | \"same-origin\" | \"no-cors\" | \"cors\"; credentials?: \"omit\" | \"same-origin\" | \"include\"; cache?: \"default\" | \"no-store\" | \"reload\" | \"no-cache\" | \"force-cache\" | \"only-if-cached\"; }","title":"configure"},{"location":"odata/queryable/#configurefrom","text":"Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance.","title":"configureFrom"},{"location":"odata/queryable/#usingcaching","text":"Enables caching for this request. See caching for more details. import { sp } from \"@pnp/sp\" sp.web.usingCaching()().then(...);","title":"usingCaching"},{"location":"odata/queryable/#inbatch","text":"Adds this query to the supplied batch","title":"inBatch"},{"location":"odata/queryable/#tourl","text":"Gets the current url","title":"toUrl"},{"location":"odata/queryable/#abstract-tourlandquery","text":"When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request","title":"abstract toUrlAndQuery()"},{"location":"odata/queryable/#get","text":"Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.","title":"get"},{"location":"pnpjs/","text":"PnPjs \u00b6 This package is a rollup package of all the other libraries for scenarios where you would prefer to access all of the code from a single file. Examples would be importing a single file into a script editor webpart or using the library in other ways that benefit from a single file. You will not be able to take advantage of selective imports using this bundle. Our recommendation is to import the packages directly into your project, or to create a custom bundle . This package is mostly provided to help folks with backward-compatibility needs. Script Editor Webpart \u00b6 The below is an example of using the pnp.js bundle within a Script Editor webpart. This script editor example is provided for folks on older version of SharePoint - when possible your first choice is SharePoint Framework. You will need to grab the pnp.js bundle file from the dist folder of the pnpjs package and upload it to a location where you can reference it from without your script editor webparts. *This is included as a reference for backward compatibility. The script editor webpart is no longer available in SharePoint online. In addition, see our General Statement on Polyfills and IE11
    Access Library Features \u00b6 Within the bundle all of the classes and methods are exported at the root object, with the exports from sp and graph libraries contained with NS variables to avoid naming conflicts. So if you need to access say the \"Web\" factory you can do so: const web = pnp.SPNS.Web(\"https://something.sharepoint.com\"); const lists = await web.lists(); pnp.GraphNS.* Individual libraries can also be accessed for their exports: pnp.Logger.subscribe(new pnp.ConsoleListener()); pnp.log.write(\"hello\");","title":"pnpjs"},{"location":"pnpjs/#pnpjs","text":"This package is a rollup package of all the other libraries for scenarios where you would prefer to access all of the code from a single file. Examples would be importing a single file into a script editor webpart or using the library in other ways that benefit from a single file. You will not be able to take advantage of selective imports using this bundle. Our recommendation is to import the packages directly into your project, or to create a custom bundle . This package is mostly provided to help folks with backward-compatibility needs.","title":"PnPjs"},{"location":"pnpjs/#script-editor-webpart","text":"The below is an example of using the pnp.js bundle within a Script Editor webpart. This script editor example is provided for folks on older version of SharePoint - when possible your first choice is SharePoint Framework. You will need to grab the pnp.js bundle file from the dist folder of the pnpjs package and upload it to a location where you can reference it from without your script editor webparts. *This is included as a reference for backward compatibility. The script editor webpart is no longer available in SharePoint online. In addition, see our General Statement on Polyfills and IE11
    ","title":"Script Editor Webpart"},{"location":"pnpjs/#access-library-features","text":"Within the bundle all of the classes and methods are exported at the root object, with the exports from sp and graph libraries contained with NS variables to avoid naming conflicts. So if you need to access say the \"Web\" factory you can do so: const web = pnp.SPNS.Web(\"https://something.sharepoint.com\"); const lists = await web.lists(); pnp.GraphNS.* Individual libraries can also be accessed for their exports: pnp.Logger.subscribe(new pnp.ConsoleListener()); pnp.log.write(\"hello\");","title":"Access Library Features"},{"location":"sp/","text":"@pnp/sp \u00b6 This package contains the fluent api used to call the SharePoint rest services. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/sp --save Import the library into your application and access the root sp object import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; (function main() { // here we will load the current web's title const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`); )() Getting Started: SharePoint Framework \u00b6 Install the library and required dependencies npm install @pnp/sp --save Import the library into your application, update OnInit, and access the root sp object in render import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; const w = await sp.web.select(\"Title\")(); this.domElement.innerHTML = `Web Title: ${w.Title}`; } Getting Started: Nodejs \u00b6 Install the library and required dependencies npm install @pnp/sp @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { SPFetchClient } from \"@pnp/nodejs\"; // do this once per page load sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{your site url}\", \"{your client id}\", \"{your client secret}\"); }, }, }); // now make any calls you need using the configured client const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`);","title":"sp"},{"location":"sp/#pnpsp","text":"This package contains the fluent api used to call the SharePoint rest services.","title":"@pnp/sp"},{"location":"sp/#getting-started","text":"Install the library and required dependencies npm install @pnp/sp --save Import the library into your application and access the root sp object import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; (function main() { // here we will load the current web's title const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`); )()","title":"Getting Started"},{"location":"sp/#getting-started-sharepoint-framework","text":"Install the library and required dependencies npm install @pnp/sp --save Import the library into your application, update OnInit, and access the root sp object in render import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; const w = await sp.web.select(\"Title\")(); this.domElement.innerHTML = `Web Title: ${w.Title}`; }","title":"Getting Started: SharePoint Framework"},{"location":"sp/#getting-started-nodejs","text":"Install the library and required dependencies npm install @pnp/sp @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { SPFetchClient } from \"@pnp/nodejs\"; // do this once per page load sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{your site url}\", \"{your client id}\", \"{your client secret}\"); }, }, }); // now make any calls you need using the configured client const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`);","title":"Getting Started: Nodejs"},{"location":"sp/alias-parameters/","text":"@pnp/sp - Aliased Parameters \u00b6 Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders. To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query. Construct a parameter alias \u00b6 Pattern: !@{label name}::{value} Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\" Example without aliasing \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // still works as expected, no aliasing const query = sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 const r = await query(); console.log(r);; Example with aliasing \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 const r = await query(); console.log(r); Example with aliasing and batching \u00b6 Aliasing is supported with batching as well: import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing and batching const batch = sp.web.createBatch(); const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query.inBatch(batch)().then(r => { console.log(r); }); batch.execute();","title":"Alias Parameters"},{"location":"sp/alias-parameters/#pnpsp-aliased-parameters","text":"Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders. To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query.","title":"@pnp/sp - Aliased Parameters"},{"location":"sp/alias-parameters/#construct-a-parameter-alias","text":"Pattern: !@{label name}::{value} Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\"","title":"Construct a parameter alias"},{"location":"sp/alias-parameters/#example-without-aliasing","text":"import { sp } from \"@pnp/sp/presets/all\"; // still works as expected, no aliasing const query = sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 const r = await query(); console.log(r);;","title":"Example without aliasing"},{"location":"sp/alias-parameters/#example-with-aliasing","text":"import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 const r = await query(); console.log(r);","title":"Example with aliasing"},{"location":"sp/alias-parameters/#example-with-aliasing-and-batching","text":"Aliasing is supported with batching as well: import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing and batching const batch = sp.web.createBatch(); const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query.inBatch(batch)().then(r => { console.log(r); }); batch.execute();","title":"Example with aliasing and batching"},{"location":"sp/alm/","text":"@pnp/sp/appcatalog \u00b6 The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions. Understanding the App Catalog Hierarchy \u00b6 Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation . Referencing an App Catalog \u00b6 There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // get the current context web's app catalog const catalog = await sp.web.getAppCatalog()(); // you can also chain off the app catalog const apps = await sp.web.getAppCatalog()(); console.log(apps); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // you can get the tenant app catalog (or any app catalog) by using the getTenantAppCatalogWeb method const appCatWeb = await sp.getTenantAppCatalogWeb()(); const appCatalog = await appCatWeb.getAppCatalog()(); // you can get the tenant app catalog (or any app catalog) by passing in a url // get the tenant app catalog const tenantCatalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/appcatalog\")(); // get a different app catalog const catalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/anothersite\")(); // alternatively you can create a new app catalog instance directly by importing the AppCatalog class import { IAppCatalog, AppCatalog } from '@pnp/sp/appcatalog'; const catalog: IAppCatalog = await AppCatalog(\"https://mytenant.sharepoint.com/sites/apps\")(); // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web import { Web } from '@pnp/sp/webs'; import { AppCatalog } from '@pnp/sp/appcatalog'; const web = Web(\"https://mytenant.sharepoint.com/sites/apps\"); const catalog = await AppCatalog(web)(); The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity. List Available Apps \u00b6 The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. // get available apps await catalog(); // get available apps selecting two fields await catalog.select(\"Title\", \"Deployed\")(); Add an App \u00b6 This action must be performed in the context of the tenant app catalog // this represents the file bytes of the app package file const blob = new Blob(); // there is an optional third argument to control overwriting existing files const r = await catalog.add(\"myapp.app\", blob); // this is at its core a file add operation so you have access to the response data as well // as a File instance representing the created file console.log(JSON.stringify(r.data, null, 4)); // all file operations are available const nameData = await r.file.select(\"Name\")(); Get an App \u00b6 You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions const app = await catalog.getAppById(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\")(); Perform app actions \u00b6 Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block. const myAppId = \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"; // deploy await catalog.getAppById(myAppId).deploy(); // retract await catalog.getAppById(myAppId).retract(); // install await catalog.getAppById(myAppId).install(); // uninstall await catalog.getAppById(myAppId).uninstall(); // upgrade await catalog.getAppById(myAppId).upgrade(); // remove await catalog.getAppById(myAppId).remove(); Synchronize a solution/app to the Microsoft Teams App Catalog \u00b6 By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id . // Using the app id await catalog.syncSolutionToTeams(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"); // Using the SharePoint apps item id await catalog.syncSolutionToTeams(\"123\", true); Notes \u00b6 The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.","title":"ALM api"},{"location":"sp/alm/#pnpspappcatalog","text":"The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.","title":"@pnp/sp/appcatalog"},{"location":"sp/alm/#understanding-the-app-catalog-hierarchy","text":"Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation .","title":"Understanding the App Catalog Hierarchy"},{"location":"sp/alm/#referencing-an-app-catalog","text":"There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // get the current context web's app catalog const catalog = await sp.web.getAppCatalog()(); // you can also chain off the app catalog const apps = await sp.web.getAppCatalog()(); console.log(apps); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // you can get the tenant app catalog (or any app catalog) by using the getTenantAppCatalogWeb method const appCatWeb = await sp.getTenantAppCatalogWeb()(); const appCatalog = await appCatWeb.getAppCatalog()(); // you can get the tenant app catalog (or any app catalog) by passing in a url // get the tenant app catalog const tenantCatalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/appcatalog\")(); // get a different app catalog const catalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/anothersite\")(); // alternatively you can create a new app catalog instance directly by importing the AppCatalog class import { IAppCatalog, AppCatalog } from '@pnp/sp/appcatalog'; const catalog: IAppCatalog = await AppCatalog(\"https://mytenant.sharepoint.com/sites/apps\")(); // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web import { Web } from '@pnp/sp/webs'; import { AppCatalog } from '@pnp/sp/appcatalog'; const web = Web(\"https://mytenant.sharepoint.com/sites/apps\"); const catalog = await AppCatalog(web)(); The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.","title":"Referencing an App Catalog"},{"location":"sp/alm/#list-available-apps","text":"The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. // get available apps await catalog(); // get available apps selecting two fields await catalog.select(\"Title\", \"Deployed\")();","title":"List Available Apps"},{"location":"sp/alm/#add-an-app","text":"This action must be performed in the context of the tenant app catalog // this represents the file bytes of the app package file const blob = new Blob(); // there is an optional third argument to control overwriting existing files const r = await catalog.add(\"myapp.app\", blob); // this is at its core a file add operation so you have access to the response data as well // as a File instance representing the created file console.log(JSON.stringify(r.data, null, 4)); // all file operations are available const nameData = await r.file.select(\"Name\")();","title":"Add an App"},{"location":"sp/alm/#get-an-app","text":"You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions const app = await catalog.getAppById(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\")();","title":"Get an App"},{"location":"sp/alm/#perform-app-actions","text":"Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block. const myAppId = \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"; // deploy await catalog.getAppById(myAppId).deploy(); // retract await catalog.getAppById(myAppId).retract(); // install await catalog.getAppById(myAppId).install(); // uninstall await catalog.getAppById(myAppId).uninstall(); // upgrade await catalog.getAppById(myAppId).upgrade(); // remove await catalog.getAppById(myAppId).remove();","title":"Perform app actions"},{"location":"sp/alm/#synchronize-a-solutionapp-to-the-microsoft-teams-app-catalog","text":"By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id . // Using the app id await catalog.syncSolutionToTeams(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"); // Using the SharePoint apps item id await catalog.syncSolutionToTeams(\"123\", true);","title":"Synchronize a solution/app to the Microsoft Teams App Catalog"},{"location":"sp/alm/#notes","text":"The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.","title":"Notes"},{"location":"sp/attachments/","text":"@pnp/sp/attachments \u00b6 The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/attachments\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\"; Get attachments \u00b6 import { sp } from \"@pnp/sp\"; import { IAttachmentInfo } from \"@pnp/sp/attachments\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); // get all the attachments const info: IAttachmentInfo[] = await item.attachmentFiles(); // get a single file by file name const info2: IAttachmentInfo = await item.attachmentFiles.getByName(\"file.txt\")(); // select specific properties using odata operators and use Pick to type the result const info3: Pick[] = await item.attachmentFiles.select(\"ServerRelativeUrl\")(); Add an Attachment \u00b6 You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.add(\"file2.txt\", \"Here is my content\"); Add Multiple \u00b6 This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists\"; import { IAttachmentFileInfo } from \"@pnp/sp/attachments\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); let fileInfos: IAttachmentFileInfo[] = []; fileInfos.push({ name: \"My file name 1\", content: \"string, blob, or array\" }); fileInfos.push({ name: \"My file name 2\", content: \"string, blob, or array\" }); await list.items.getById(2).attachmentFiles.addMultiple(fileInfos); Delete Multiple \u00b6 import { sp } from \"@pnp/sp\"; import { IList } from \"./@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.deleteMultiple(\"1.txt\", \"2.txt\"); Read Attachment Content \u00b6 You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); const text = await item.attachmentFiles.getByName(\"file.txt\").getText(); // use this in the browser, does not work in nodejs const blob = await item.attachmentFiles.getByName(\"file.mp4\").getBlob(); // use this in nodejs const buffer = await item.attachmentFiles.getByName(\"file.mp4\").getBuffer(); // file must be valid json const json = await item.attachmentFiles.getByName(\"file.json\").getJSON(); Update Attachment Content \u00b6 You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").setContent(\"My new content!!!\"); Delete Attachment \u00b6 import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").delete(); Recycle Attachment \u00b6 Delete the attachment and send it to recycle bin import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").recycle(); Recycle Multiple Attachments \u00b6 Delete multiple attachments and send them to recycle bin import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.recycleMultiple(\"1.txt\",\"2.txt\");","title":"Attachments"},{"location":"sp/attachments/#pnpspattachments","text":"The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/attachments\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/attachments"},{"location":"sp/attachments/#get-attachments","text":"import { sp } from \"@pnp/sp\"; import { IAttachmentInfo } from \"@pnp/sp/attachments\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); // get all the attachments const info: IAttachmentInfo[] = await item.attachmentFiles(); // get a single file by file name const info2: IAttachmentInfo = await item.attachmentFiles.getByName(\"file.txt\")(); // select specific properties using odata operators and use Pick to type the result const info3: Pick[] = await item.attachmentFiles.select(\"ServerRelativeUrl\")();","title":"Get attachments"},{"location":"sp/attachments/#add-an-attachment","text":"You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.add(\"file2.txt\", \"Here is my content\");","title":"Add an Attachment"},{"location":"sp/attachments/#add-multiple","text":"This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists\"; import { IAttachmentFileInfo } from \"@pnp/sp/attachments\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); let fileInfos: IAttachmentFileInfo[] = []; fileInfos.push({ name: \"My file name 1\", content: \"string, blob, or array\" }); fileInfos.push({ name: \"My file name 2\", content: \"string, blob, or array\" }); await list.items.getById(2).attachmentFiles.addMultiple(fileInfos);","title":"Add Multiple"},{"location":"sp/attachments/#delete-multiple","text":"import { sp } from \"@pnp/sp\"; import { IList } from \"./@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.deleteMultiple(\"1.txt\", \"2.txt\");","title":"Delete Multiple"},{"location":"sp/attachments/#read-attachment-content","text":"You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); const text = await item.attachmentFiles.getByName(\"file.txt\").getText(); // use this in the browser, does not work in nodejs const blob = await item.attachmentFiles.getByName(\"file.mp4\").getBlob(); // use this in nodejs const buffer = await item.attachmentFiles.getByName(\"file.mp4\").getBuffer(); // file must be valid json const json = await item.attachmentFiles.getByName(\"file.json\").getJSON();","title":"Read Attachment Content"},{"location":"sp/attachments/#update-attachment-content","text":"You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").setContent(\"My new content!!!\");","title":"Update Attachment Content"},{"location":"sp/attachments/#delete-attachment","text":"import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").delete();","title":"Delete Attachment"},{"location":"sp/attachments/#recycle-attachment","text":"Delete the attachment and send it to recycle bin import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").recycle();","title":"Recycle Attachment"},{"location":"sp/attachments/#recycle-multiple-attachments","text":"Delete multiple attachments and send them to recycle bin import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.recycleMultiple(\"1.txt\",\"2.txt\");","title":"Recycle Multiple Attachments"},{"location":"sp/clientside-pages/","text":"@pnp/sp/clientside-pages \u00b6 The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/clientside-pages\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/clientside-pages\"; Preset: All import { sp, ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/presets/all\"; Create a new Page \u00b6 You can create a new client-side page in several ways, all are equivalent. Create using IWeb.addClientsidePage \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { PromotedState } from \"@pnp/sp/clientside-pages\"; // Create a page providing a file name const page = await sp.web.addClientsidePage(\"mypage1\"); // ... other operations on the page as outlined below // the page is initially not published, you must publish it so it appears for others users await page.save(); // include title and page layout const page2 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // include title, page layout, and specifying the publishing status (Added in 2.0.4) const page3 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page3.save(); Create using CreateClientsidePage method \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import { CreateClientsidePage, PromotedState } from \"@pnp/sp/clientside-pages\"; const page1 = await CreateClientsidePage(sp.web, \"mypage2\", \"My Page Title\"); // you must publish the new page await page1.save(true); // specify the page layout type parameter const page2 = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4) const page2half = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page2half.save(); // use the web factory to create a page in a specific web const page3 = await CreateClientsidePage(Web(\"https://{absolute web url}\"), \"mypage4\", \"My Page Title\"); // you must publish the new page await page3.save(); Load Pages \u00b6 There are a few ways to load pages, each of which results in an IClientsidePage instance being returned. Load using IWeb.loadClientsidePage \u00b6 This method takes a server relative path to the page to load. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // use from the sp.web fluent chain const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); // use the web factory to target a specific web const page2 = await Web(\"https://{absolute web url}\").loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); Load using ClientsidePageFromFile \u00b6 This method takes an IFile instance and loads an IClientsidePage instance. import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath(\"/sites/dev/sitepages/mypage3.aspx\")); Edit Sections and Columns \u00b6 Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections. // our page instance const page: IClientsidePage; // add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12 const section1 = page.addSection(); section1.addColumn(6); section1.addColumn(6); // create a three column layout in a new section const section2 = page.addSection(); section2.addColumn(4); section2.addColumn(4); section2.addColumn(4); // publish our changes await page.save(); Manipulate Sections and Columns \u00b6 // our page instance const page: IClientsidePage; // drop all the columns in this section // this will also DELETE all controls contained in the columns page.sections[1].columns.length = 0; // create a new column layout page.sections[1].addColumn(4); page.sections[1].addColumn(8); // publish our changes await page.save(); Vertical Section \u00b6 The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier. // our page instance const page: IClientsidePage; // add or get a vertical section (handles case where section already exists) const vertSection = page.addVerticalSection(); // **************************************************************** // if you know or want to test if a vertical section is present: if (page.hasVerticalSection) { // access the vertical section (this method will NOT create the section if it does not exist) page.verticalSection.addControl(new ClientsideText(\"hello\")); } else { const vertSection = page.addVerticalSection(); vertSection.addControl(new ClientsideText(\"hello\")); } Reorder Sections \u00b6 // our page instance const page: IClientsidePage; // swap the order of two sections // this will preserve the controls within the columns page.sections = [page.sections[1], page.sections[0]]; // publish our changes await page.save(); Reorder Columns \u00b6 The sections and columns are arrays, so normal array operations work as expected // our page instance const page: IClientsidePage; // swap the order of two columns // this will preserve the controls within the columns page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]]; // publish our changes await page.save(); Clientside Controls \u00b6 Once you have your sections and columns defined you will want to add/edit controls within those columns. Add Text Content \u00b6 import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; page.addSection().addControl(new ClientsideText(\"@pnp/sp is a great library!\")); await page.save(); Add Controls \u00b6 Adding controls involves loading the available client-side part definitions from the server or creating a text part. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // this will be a ClientsidePageComponent array // this can be cached on the client in production scenarios const partDefs = await sp.web.getClientsideWebParts(); // find the definition we want, here by id const partDef = partDefs.filter(c => c.Id === \"490d7c76-1824-45b2-9de3-676421c997fa\"); // optionally ensure you found the def if (partDef.length < 1) { // we didn't find it so we throw an error throw new Error(\"Could not find the web part\"); } // create a ClientWebPart instance from the definition const part = ClientsideWebpart.fromComponentDef(partDef[0]); // set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video. // the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting // the properties correctly part.setProperties<{ embedCode: string }>({ embedCode: \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\", }); // we add that part to a new section page.addSection().addControl(part); await page.save(); Handle Different Webpart's Settings \u00b6 There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // we create a class to wrap our functionality in a reusable way class ListWebpart extends ClientsideWebpart { constructor(control: ClientsideWebpart) { super((control).json); } // add property getter/setter for what we need, in this case \"listTitle\" within searchablePlainTexts public get DisplayTitle(): string { return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || \"\"; } public set DisplayTitle(value: string) { this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value; } } // now we load our page const page = await sp.web.loadClientsidePage(\"/sites/dev/SitePages/List-Web-Part.aspx\"); // get our part and pass it to the constructor of our wrapper class const part = new ListWebpart(page.sections[0].columns[0].getControl(0)); part.DisplayTitle = \"My New Title!\"; await page.save(); Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties. Page Operations \u00b6 There are other operation you can perform on a page in addition to manipulating the content. pageLayout \u00b6 You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously. // our page instance const page: IClientsidePage; // get the current value const value = page.pageLayout; // set the value page.pageLayout = \"Article\"; await page.save(); bannerImageUrl \u00b6 // our page instance const page: IClientsidePage; // get the current value const value = page.bannerImageUrl; // set the value page.bannerImageUrl = \"/server/relative/path/to/image.png\"; await page.save(); Banner images need to exist within the same site collection as the page where you want to use them. thumbnailUrl \u00b6 Allows you to set the thumbnail used for the page independently of the banner. If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality. // our page instance const page: IClientsidePage; // get the current value const value = page.thumbnailUrl; // set the value page.thumbnailUrl = \"/server/relative/path/to/image.png\"; await page.save(); topicHeader \u00b6 // our page instance const page: IClientsidePage; // get the current value const value = page.topicHeader; // set the value page.topicHeader = \"My cool header!\"; await page.save(); // clear the topic header and hide it page.topicHeader = \"\"; await page.save(); title \u00b6 // our page instance const page: IClientsidePage; // get the current value const value = page.title; // set the value page.title = \"My page title\"; await page.save(); description \u00b6 Descriptions are limited to 255 chars // our page instance const page: IClientsidePage; // get the current value const value = page.description; // set the value page.description = \"A description\"; await page.save(); layoutType \u00b6 Sets the layout type of the page. The valid values are: \"FullWidthImage\", \"NoImage\", \"ColorBlock\", \"CutInShape\" // our page instance const page: IClientsidePage; // get the current value const value = page.layoutType; // set the value page.layoutType = \"ColorBlock\"; await page.save(); headerTextAlignment \u00b6 Sets the header text alignment to one of \"Left\" or \"Center\" // our page instance const page: IClientsidePage; // get the current value const value = page.headerTextAlignment; // set the value page.headerTextAlignment = \"Center\"; await page.save(); showTopicHeader \u00b6 Sets if the topic header is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showTopicHeader; // show the header page.showTopicHeader = true; await page.save(); // hide the header page.showTopicHeader = false; await page.save(); showPublishDate \u00b6 Sets if the publish date is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showPublishDate; // show the date page.showPublishDate = true; await page.save(); // hide the date page.showPublishDate = false; await page.save(); Get / Set author details \u00b6 Added in 2.0.4 // our page instance const page: IClientsidePage; // get the author details (string | null) const value = page.authorByLine; // set the author by user id const user = await web.currentUser.select(\"Id\", \"LoginName\")(); const userId = user.Id; const userLogin = user.LoginName; await page.setAuthorById(userId); await page.save(); await page.setAuthorByLoginName(userLogin); await page.save(); you must still save the page after setting the author to persist your changes as shown in the example. load \u00b6 Loads the page from the server. This will overwrite any local unsaved changes. // our page instance const page: IClientsidePage; await page.load(); save \u00b6 Saves any changes to the page, optionally keeping them in draft state. // our page instance const page: IClientsidePage; // changes are published await page.save(); // changes remain in draft await page.save(false); discardPageCheckout \u00b6 Discards any current checkout of the page by the current user. // our page instance const page: IClientsidePage; await page.discardPageCheckout(); promoteToNews \u00b6 Promotes the page as a news article. // our page instance const page: IClientsidePage; await page.promoteToNews(); enableComments & disableComments \u00b6 Used to control the availability of comments on a page. // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments(); findControlById \u00b6 Finds a control within the page by id. import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\"); // you can also type the control const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\"); findControl \u00b6 Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page. // our page instance const page: IClientsidePage; // find the first control whose order is 9 const control = page.findControl((c) => c.order === 9); // iterate all the controls and output the id to the console page.findControl((c) => { console.log(c.id); return false; }); like & unlike \u00b6 Updates the page's like value for the current user. // our page instance const page: IClientsidePage; // like this page await page.like(); // unlike this page await page.unlike(); getLikedByInformation \u00b6 Gets the likes information for this page. // our page instance const page: IClientsidePage; const info = await page.getLikedByInformation(); copy \u00b6 Creates a copy of the page, including all controls. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instance const page: IClientsidePage; // creates a published copy of the page const pageCopy = await page.copy(sp.web, \"newpagename\", \"New Page Title\"); // creates a draft (unpublished) copy of the page const pageCopy2 = await page.copy(sp.web, \"newpagename\", \"New Page Title\", false); // edits to pageCopy2 ... // publish the page pageCopy2.save(); copyTo \u00b6 Copies the contents of a page to another existing page instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instances, loaded in any of the ways shown above const source: IClientsidePage; const target: IClientsidePage; const target2: IClientsidePage; // creates a published copy of the page await source.copyTo(target); // creates a draft (unpublished) copy of the page await source.copyTo(target2, false); // edits to target2... // publish the page target2.save(); setBannerImage \u00b6 Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent. Banner images need to exist within the same site collection as the page where you want to use them. // our page instance const page: IClientsidePage; page.setBannerImage(\"/server/relative/path/to/image.png\"); // save the changes await page.save(); // set additional props page.setBannerImage(\"/server/relative/path/to/image.png\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save(); This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar. import { SPFetchClient } from \"@pnp/nodejs\"; import { join } from \"path\"; import { readFileSync } from \"fs\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/clientside-pages\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{Site Url}\", \"{Client Id}\", \"{Client Secret}\"); }, }, }); // add the banner image const dirname = join(\"C:/path/to/file\", \"img-file.jpg\"); const file: Uint8Array = new Uint8Array(readFileSync(dirname)); const far = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents\").files.add(\"banner.jpg\", file, true); // add the page const page = await sp.web.addClientsidePage(\"MyPage\", \"Page Title\"); // set the banner image page.setBannerImage(far.data.ServerRelativeUrl); // publish the page await page.save(); setBannerImageFromExternalUrl \u00b6 Added in 2.0.12 Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there. // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\"); // save the changes await page.save(); You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save();","title":"Client-side Pages"},{"location":"sp/clientside-pages/#pnpspclientside-pages","text":"The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/clientside-pages\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/clientside-pages\"; Preset: All import { sp, ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/clientside-pages"},{"location":"sp/clientside-pages/#create-a-new-page","text":"You can create a new client-side page in several ways, all are equivalent.","title":"Create a new Page"},{"location":"sp/clientside-pages/#create-using-iwebaddclientsidepage","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { PromotedState } from \"@pnp/sp/clientside-pages\"; // Create a page providing a file name const page = await sp.web.addClientsidePage(\"mypage1\"); // ... other operations on the page as outlined below // the page is initially not published, you must publish it so it appears for others users await page.save(); // include title and page layout const page2 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // include title, page layout, and specifying the publishing status (Added in 2.0.4) const page3 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page3.save();","title":"Create using IWeb.addClientsidePage"},{"location":"sp/clientside-pages/#create-using-createclientsidepage-method","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import { CreateClientsidePage, PromotedState } from \"@pnp/sp/clientside-pages\"; const page1 = await CreateClientsidePage(sp.web, \"mypage2\", \"My Page Title\"); // you must publish the new page await page1.save(true); // specify the page layout type parameter const page2 = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4) const page2half = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page2half.save(); // use the web factory to create a page in a specific web const page3 = await CreateClientsidePage(Web(\"https://{absolute web url}\"), \"mypage4\", \"My Page Title\"); // you must publish the new page await page3.save();","title":"Create using CreateClientsidePage method"},{"location":"sp/clientside-pages/#load-pages","text":"There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.","title":"Load Pages"},{"location":"sp/clientside-pages/#load-using-iwebloadclientsidepage","text":"This method takes a server relative path to the page to load. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // use from the sp.web fluent chain const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); // use the web factory to target a specific web const page2 = await Web(\"https://{absolute web url}\").loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");","title":"Load using IWeb.loadClientsidePage"},{"location":"sp/clientside-pages/#load-using-clientsidepagefromfile","text":"This method takes an IFile instance and loads an IClientsidePage instance. import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath(\"/sites/dev/sitepages/mypage3.aspx\"));","title":"Load using ClientsidePageFromFile"},{"location":"sp/clientside-pages/#edit-sections-and-columns","text":"Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections. // our page instance const page: IClientsidePage; // add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12 const section1 = page.addSection(); section1.addColumn(6); section1.addColumn(6); // create a three column layout in a new section const section2 = page.addSection(); section2.addColumn(4); section2.addColumn(4); section2.addColumn(4); // publish our changes await page.save();","title":"Edit Sections and Columns"},{"location":"sp/clientside-pages/#manipulate-sections-and-columns","text":"// our page instance const page: IClientsidePage; // drop all the columns in this section // this will also DELETE all controls contained in the columns page.sections[1].columns.length = 0; // create a new column layout page.sections[1].addColumn(4); page.sections[1].addColumn(8); // publish our changes await page.save();","title":"Manipulate Sections and Columns"},{"location":"sp/clientside-pages/#vertical-section","text":"The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier. // our page instance const page: IClientsidePage; // add or get a vertical section (handles case where section already exists) const vertSection = page.addVerticalSection(); // **************************************************************** // if you know or want to test if a vertical section is present: if (page.hasVerticalSection) { // access the vertical section (this method will NOT create the section if it does not exist) page.verticalSection.addControl(new ClientsideText(\"hello\")); } else { const vertSection = page.addVerticalSection(); vertSection.addControl(new ClientsideText(\"hello\")); }","title":"Vertical Section"},{"location":"sp/clientside-pages/#reorder-sections","text":"// our page instance const page: IClientsidePage; // swap the order of two sections // this will preserve the controls within the columns page.sections = [page.sections[1], page.sections[0]]; // publish our changes await page.save();","title":"Reorder Sections"},{"location":"sp/clientside-pages/#reorder-columns","text":"The sections and columns are arrays, so normal array operations work as expected // our page instance const page: IClientsidePage; // swap the order of two columns // this will preserve the controls within the columns page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]]; // publish our changes await page.save();","title":"Reorder Columns"},{"location":"sp/clientside-pages/#clientside-controls","text":"Once you have your sections and columns defined you will want to add/edit controls within those columns.","title":"Clientside Controls"},{"location":"sp/clientside-pages/#add-text-content","text":"import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; page.addSection().addControl(new ClientsideText(\"@pnp/sp is a great library!\")); await page.save();","title":"Add Text Content"},{"location":"sp/clientside-pages/#add-controls","text":"Adding controls involves loading the available client-side part definitions from the server or creating a text part. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // this will be a ClientsidePageComponent array // this can be cached on the client in production scenarios const partDefs = await sp.web.getClientsideWebParts(); // find the definition we want, here by id const partDef = partDefs.filter(c => c.Id === \"490d7c76-1824-45b2-9de3-676421c997fa\"); // optionally ensure you found the def if (partDef.length < 1) { // we didn't find it so we throw an error throw new Error(\"Could not find the web part\"); } // create a ClientWebPart instance from the definition const part = ClientsideWebpart.fromComponentDef(partDef[0]); // set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video. // the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting // the properties correctly part.setProperties<{ embedCode: string }>({ embedCode: \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\", }); // we add that part to a new section page.addSection().addControl(part); await page.save();","title":"Add Controls"},{"location":"sp/clientside-pages/#handle-different-webparts-settings","text":"There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // we create a class to wrap our functionality in a reusable way class ListWebpart extends ClientsideWebpart { constructor(control: ClientsideWebpart) { super((control).json); } // add property getter/setter for what we need, in this case \"listTitle\" within searchablePlainTexts public get DisplayTitle(): string { return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || \"\"; } public set DisplayTitle(value: string) { this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value; } } // now we load our page const page = await sp.web.loadClientsidePage(\"/sites/dev/SitePages/List-Web-Part.aspx\"); // get our part and pass it to the constructor of our wrapper class const part = new ListWebpart(page.sections[0].columns[0].getControl(0)); part.DisplayTitle = \"My New Title!\"; await page.save(); Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.","title":"Handle Different Webpart's Settings"},{"location":"sp/clientside-pages/#page-operations","text":"There are other operation you can perform on a page in addition to manipulating the content.","title":"Page Operations"},{"location":"sp/clientside-pages/#pagelayout","text":"You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously. // our page instance const page: IClientsidePage; // get the current value const value = page.pageLayout; // set the value page.pageLayout = \"Article\"; await page.save();","title":"pageLayout"},{"location":"sp/clientside-pages/#bannerimageurl","text":"// our page instance const page: IClientsidePage; // get the current value const value = page.bannerImageUrl; // set the value page.bannerImageUrl = \"/server/relative/path/to/image.png\"; await page.save(); Banner images need to exist within the same site collection as the page where you want to use them.","title":"bannerImageUrl"},{"location":"sp/clientside-pages/#thumbnailurl","text":"Allows you to set the thumbnail used for the page independently of the banner. If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality. // our page instance const page: IClientsidePage; // get the current value const value = page.thumbnailUrl; // set the value page.thumbnailUrl = \"/server/relative/path/to/image.png\"; await page.save();","title":"thumbnailUrl"},{"location":"sp/clientside-pages/#topicheader","text":"// our page instance const page: IClientsidePage; // get the current value const value = page.topicHeader; // set the value page.topicHeader = \"My cool header!\"; await page.save(); // clear the topic header and hide it page.topicHeader = \"\"; await page.save();","title":"topicHeader"},{"location":"sp/clientside-pages/#title","text":"// our page instance const page: IClientsidePage; // get the current value const value = page.title; // set the value page.title = \"My page title\"; await page.save();","title":"title"},{"location":"sp/clientside-pages/#description","text":"Descriptions are limited to 255 chars // our page instance const page: IClientsidePage; // get the current value const value = page.description; // set the value page.description = \"A description\"; await page.save();","title":"description"},{"location":"sp/clientside-pages/#layouttype","text":"Sets the layout type of the page. The valid values are: \"FullWidthImage\", \"NoImage\", \"ColorBlock\", \"CutInShape\" // our page instance const page: IClientsidePage; // get the current value const value = page.layoutType; // set the value page.layoutType = \"ColorBlock\"; await page.save();","title":"layoutType"},{"location":"sp/clientside-pages/#headertextalignment","text":"Sets the header text alignment to one of \"Left\" or \"Center\" // our page instance const page: IClientsidePage; // get the current value const value = page.headerTextAlignment; // set the value page.headerTextAlignment = \"Center\"; await page.save();","title":"headerTextAlignment"},{"location":"sp/clientside-pages/#showtopicheader","text":"Sets if the topic header is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showTopicHeader; // show the header page.showTopicHeader = true; await page.save(); // hide the header page.showTopicHeader = false; await page.save();","title":"showTopicHeader"},{"location":"sp/clientside-pages/#showpublishdate","text":"Sets if the publish date is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showPublishDate; // show the date page.showPublishDate = true; await page.save(); // hide the date page.showPublishDate = false; await page.save();","title":"showPublishDate"},{"location":"sp/clientside-pages/#get-set-author-details","text":"Added in 2.0.4 // our page instance const page: IClientsidePage; // get the author details (string | null) const value = page.authorByLine; // set the author by user id const user = await web.currentUser.select(\"Id\", \"LoginName\")(); const userId = user.Id; const userLogin = user.LoginName; await page.setAuthorById(userId); await page.save(); await page.setAuthorByLoginName(userLogin); await page.save(); you must still save the page after setting the author to persist your changes as shown in the example.","title":"Get / Set author details"},{"location":"sp/clientside-pages/#load","text":"Loads the page from the server. This will overwrite any local unsaved changes. // our page instance const page: IClientsidePage; await page.load();","title":"load"},{"location":"sp/clientside-pages/#save","text":"Saves any changes to the page, optionally keeping them in draft state. // our page instance const page: IClientsidePage; // changes are published await page.save(); // changes remain in draft await page.save(false);","title":"save"},{"location":"sp/clientside-pages/#discardpagecheckout","text":"Discards any current checkout of the page by the current user. // our page instance const page: IClientsidePage; await page.discardPageCheckout();","title":"discardPageCheckout"},{"location":"sp/clientside-pages/#promotetonews","text":"Promotes the page as a news article. // our page instance const page: IClientsidePage; await page.promoteToNews();","title":"promoteToNews"},{"location":"sp/clientside-pages/#enablecomments-disablecomments","text":"Used to control the availability of comments on a page. // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments();","title":"enableComments & disableComments"},{"location":"sp/clientside-pages/#findcontrolbyid","text":"Finds a control within the page by id. import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\"); // you can also type the control const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\");","title":"findControlById"},{"location":"sp/clientside-pages/#findcontrol","text":"Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page. // our page instance const page: IClientsidePage; // find the first control whose order is 9 const control = page.findControl((c) => c.order === 9); // iterate all the controls and output the id to the console page.findControl((c) => { console.log(c.id); return false; });","title":"findControl"},{"location":"sp/clientside-pages/#like-unlike","text":"Updates the page's like value for the current user. // our page instance const page: IClientsidePage; // like this page await page.like(); // unlike this page await page.unlike();","title":"like & unlike"},{"location":"sp/clientside-pages/#getlikedbyinformation","text":"Gets the likes information for this page. // our page instance const page: IClientsidePage; const info = await page.getLikedByInformation();","title":"getLikedByInformation"},{"location":"sp/clientside-pages/#copy","text":"Creates a copy of the page, including all controls. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instance const page: IClientsidePage; // creates a published copy of the page const pageCopy = await page.copy(sp.web, \"newpagename\", \"New Page Title\"); // creates a draft (unpublished) copy of the page const pageCopy2 = await page.copy(sp.web, \"newpagename\", \"New Page Title\", false); // edits to pageCopy2 ... // publish the page pageCopy2.save();","title":"copy"},{"location":"sp/clientside-pages/#copyto","text":"Copies the contents of a page to another existing page instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instances, loaded in any of the ways shown above const source: IClientsidePage; const target: IClientsidePage; const target2: IClientsidePage; // creates a published copy of the page await source.copyTo(target); // creates a draft (unpublished) copy of the page await source.copyTo(target2, false); // edits to target2... // publish the page target2.save();","title":"copyTo"},{"location":"sp/clientside-pages/#setbannerimage","text":"Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent. Banner images need to exist within the same site collection as the page where you want to use them. // our page instance const page: IClientsidePage; page.setBannerImage(\"/server/relative/path/to/image.png\"); // save the changes await page.save(); // set additional props page.setBannerImage(\"/server/relative/path/to/image.png\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save(); This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar. import { SPFetchClient } from \"@pnp/nodejs\"; import { join } from \"path\"; import { readFileSync } from \"fs\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/clientside-pages\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{Site Url}\", \"{Client Id}\", \"{Client Secret}\"); }, }, }); // add the banner image const dirname = join(\"C:/path/to/file\", \"img-file.jpg\"); const file: Uint8Array = new Uint8Array(readFileSync(dirname)); const far = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents\").files.add(\"banner.jpg\", file, true); // add the page const page = await sp.web.addClientsidePage(\"MyPage\", \"Page Title\"); // set the banner image page.setBannerImage(far.data.ServerRelativeUrl); // publish the page await page.save();","title":"setBannerImage"},{"location":"sp/clientside-pages/#setbannerimagefromexternalurl","text":"Added in 2.0.12 Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there. // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\"); // save the changes await page.save(); You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save();","title":"setBannerImageFromExternalUrl"},{"location":"sp/column-defaults/","text":"@pnp/sp/column-defaults \u00b6 The column defaults sub-module allows you to manage the default column values on a library or library folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/column-defaults\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/column-defaults\"; Preset: All import { sp, IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/presents/all\"; Get Folder Defaults \u00b6 You can get the default values for a specific folder as shown below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" } ] */ Set Folder Defaults \u00b6 When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").setDefaultColumnValues([{ name: \"TextField\", value: \"Something\", }, { name: \"NumberField\", value: 14, }]); Get Library Defaults \u00b6 You can also get all of the defaults for the entire library. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.lists.getByTitle(\"DefaultColumnValues\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{a different default value}\" } ] */ Set Library Defaults \u00b6 You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([{ name: \"TextField\", path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }]); Clear Folder Defaults \u00b6 If you want to clear all of the folder defaults you can use the clear method: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").clearDefaultColumnValues(); Clear Library Defaults \u00b6 If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([]); Pattern for setting defaults on various column types \u00b6 The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types. [{ // Text/Boolean/CurrencyDateTime/Choice/User name: \"TextField\": path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }, { //Number name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: 42, }, { //Date name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"1900-01-01T00:00:00Z\", }, { //Date - Today name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"[today]\", }, { //MultiChoice name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues\", value: [\"Item 1\", \"Item 2\"], }, { //MultiChoice - single value name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues/folder2\", value: [\"Item 1\"], }, { //Taxonomy - single value name: \"TaxonomyField\", path: \"/sites/dev/DefaultColumnValues\", value: { wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" } }, { //Taxonomy - multiple value name: \"TaxonomyMultiField\", path: \"/sites/dev/DefaultColumnValues\", value: [{ wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" },{ wssId:\"-1\", termName: \"TaxValueName2\", termId: \"95d4c307-dde5-49d8-b861-392e145d94d3\" },] }]); Taxonomy Full Example \u00b6 This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/column-defaults\"; import \"@pnp/sp/taxonomy\"; // get the term's info we want to use as the default const term = await sp.termStore.sets.getById(\"ea6fc521-d293-4f3d-9e84-f3a5bc0936ce\").getTermById(\"775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a\")(); // get the default term label const defLabel = term.labels.find(v => v.isDefault); // set the default value using -1, the term id, and the term's default label name await sp.web.lists.getByTitle(\"MetaDataDocLib\").rootFolder.setDefaultColumnValues([{ name: \"MetaDataColumnInternalName\", value: { wssId: \"-1\", termId: term.id, termName: defLabel.name, } }]) // check that the defaults have updated const newDefaults = await sp.web.lists.getByTitle(\"MetaDataDocLib\").getDefaultColumnValues();","title":"Column Defaults"},{"location":"sp/column-defaults/#pnpspcolumn-defaults","text":"The column defaults sub-module allows you to manage the default column values on a library or library folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/column-defaults\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/column-defaults\"; Preset: All import { sp, IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/presents/all\";","title":"@pnp/sp/column-defaults"},{"location":"sp/column-defaults/#get-folder-defaults","text":"You can get the default values for a specific folder as shown below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" } ] */","title":"Get Folder Defaults"},{"location":"sp/column-defaults/#set-folder-defaults","text":"When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").setDefaultColumnValues([{ name: \"TextField\", value: \"Something\", }, { name: \"NumberField\", value: 14, }]);","title":"Set Folder Defaults"},{"location":"sp/column-defaults/#get-library-defaults","text":"You can also get all of the defaults for the entire library. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.lists.getByTitle(\"DefaultColumnValues\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{a different default value}\" } ] */","title":"Get Library Defaults"},{"location":"sp/column-defaults/#set-library-defaults","text":"You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([{ name: \"TextField\", path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }]);","title":"Set Library Defaults"},{"location":"sp/column-defaults/#clear-folder-defaults","text":"If you want to clear all of the folder defaults you can use the clear method: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").clearDefaultColumnValues();","title":"Clear Folder Defaults"},{"location":"sp/column-defaults/#clear-library-defaults","text":"If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([]);","title":"Clear Library Defaults"},{"location":"sp/column-defaults/#pattern-for-setting-defaults-on-various-column-types","text":"The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types. [{ // Text/Boolean/CurrencyDateTime/Choice/User name: \"TextField\": path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }, { //Number name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: 42, }, { //Date name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"1900-01-01T00:00:00Z\", }, { //Date - Today name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"[today]\", }, { //MultiChoice name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues\", value: [\"Item 1\", \"Item 2\"], }, { //MultiChoice - single value name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues/folder2\", value: [\"Item 1\"], }, { //Taxonomy - single value name: \"TaxonomyField\", path: \"/sites/dev/DefaultColumnValues\", value: { wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" } }, { //Taxonomy - multiple value name: \"TaxonomyMultiField\", path: \"/sites/dev/DefaultColumnValues\", value: [{ wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" },{ wssId:\"-1\", termName: \"TaxValueName2\", termId: \"95d4c307-dde5-49d8-b861-392e145d94d3\" },] }]);","title":"Pattern for setting defaults on various column types"},{"location":"sp/column-defaults/#taxonomy-full-example","text":"This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/column-defaults\"; import \"@pnp/sp/taxonomy\"; // get the term's info we want to use as the default const term = await sp.termStore.sets.getById(\"ea6fc521-d293-4f3d-9e84-f3a5bc0936ce\").getTermById(\"775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a\")(); // get the default term label const defLabel = term.labels.find(v => v.isDefault); // set the default value using -1, the term id, and the term's default label name await sp.web.lists.getByTitle(\"MetaDataDocLib\").rootFolder.setDefaultColumnValues([{ name: \"MetaDataColumnInternalName\", value: { wssId: \"-1\", termId: term.id, termName: defLabel.name, } }]) // check that the defaults have updated const newDefaults = await sp.web.lists.getByTitle(\"MetaDataDocLib\").getDefaultColumnValues();","title":"Taxonomy Full Example"},{"location":"sp/comments-likes/","text":"@pnp/sp/comments and likes \u00b6 Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles. These APIs are currently in BETA and are subject to change or may not work on all tenants. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; ClientsidePage Comments \u00b6 The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately. Add Comments \u00b6 You can add a comment using the addComment method as shown import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\"); Get Page Comments \u00b6 import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); const comments = await page.getComments(); enableComments & disableComments \u00b6 Used to control the availability of comments on a page // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments(); GetById \u00b6 import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\"); const commentData = await page.getCommentById(parseInt(comment.id, 10)); Clear Comments \u00b6 Item Comments \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/comments/item\"; const item = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/SitePages/Test_8q5L.aspx\").getItem(); // as an example, or any of the below options await item.like(); The below examples use a variable named \"item\" which is taken to represent an IItem instance. Comments \u00b6 Get Item Comments \u00b6 const comments = await item.comments(); You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: import { spODataEntityArray } from \"@pnp/sp/odata\"; import { Comment, ICommentData } from \"@pnp/sp/comments\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" }); //load the top 20 replies and comments for an item including likedBy information const comments = await item.comments.expand(\"replies\", \"likedBy\", \"replies/likedBy\").top(20)(); Add Comment \u00b6 // you can add a comment as a string item.comments.add(\"string comment\"); // or you can add it as an object to include mentions item.comments.add({ text: \"comment from object property\" }); Delete a Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].delete() Like Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].like() Unlike Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); comments[0].unlike() Reply to a Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const comment: Comment & CommentData = await comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" }); Load Replies to a Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const replies = await comments[0].replies(); Like \u00b6 You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/item\"; import { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\"; // like an item await item.like(); // unlike an item await item.unlike(); // get the liked by data const likedByData: ILikeData[] = await item.getLikedBy(); // get the liked by information const likedByInfo: ILikedByInformation = await item.getLikedByInformation(); To like/unlike a client-side page and get liked by information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/clientside-page\"; import { ILikedByInformation } from \"@pnp/sp/comments\"; // like a page await page.like(); // unlike a page await page.unlike(); // get the liked by information const likedByInfo: ILikedByInformation = await page.getLikedByInformation();","title":"Comments and Likes"},{"location":"sp/comments-likes/#pnpspcomments-and-likes","text":"Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles. These APIs are currently in BETA and are subject to change or may not work on all tenants. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/comments and likes"},{"location":"sp/comments-likes/#clientsidepage-comments","text":"The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.","title":"ClientsidePage Comments"},{"location":"sp/comments-likes/#add-comments","text":"You can add a comment using the addComment method as shown import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\");","title":"Add Comments"},{"location":"sp/comments-likes/#get-page-comments","text":"import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); const comments = await page.getComments();","title":"Get Page Comments"},{"location":"sp/comments-likes/#enablecomments-disablecomments","text":"Used to control the availability of comments on a page // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments();","title":"enableComments & disableComments"},{"location":"sp/comments-likes/#getbyid","text":"import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\"); const commentData = await page.getCommentById(parseInt(comment.id, 10));","title":"GetById"},{"location":"sp/comments-likes/#clear-comments","text":"","title":"Clear Comments"},{"location":"sp/comments-likes/#item-comments","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/comments/item\"; const item = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/SitePages/Test_8q5L.aspx\").getItem(); // as an example, or any of the below options await item.like(); The below examples use a variable named \"item\" which is taken to represent an IItem instance.","title":"Item Comments"},{"location":"sp/comments-likes/#comments","text":"","title":"Comments"},{"location":"sp/comments-likes/#get-item-comments","text":"const comments = await item.comments(); You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: import { spODataEntityArray } from \"@pnp/sp/odata\"; import { Comment, ICommentData } from \"@pnp/sp/comments\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" }); //load the top 20 replies and comments for an item including likedBy information const comments = await item.comments.expand(\"replies\", \"likedBy\", \"replies/likedBy\").top(20)();","title":"Get Item Comments"},{"location":"sp/comments-likes/#add-comment","text":"// you can add a comment as a string item.comments.add(\"string comment\"); // or you can add it as an object to include mentions item.comments.add({ text: \"comment from object property\" });","title":"Add Comment"},{"location":"sp/comments-likes/#delete-a-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].delete()","title":"Delete a Comment"},{"location":"sp/comments-likes/#like-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].like()","title":"Like Comment"},{"location":"sp/comments-likes/#unlike-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); comments[0].unlike()","title":"Unlike Comment"},{"location":"sp/comments-likes/#reply-to-a-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const comment: Comment & CommentData = await comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" });","title":"Reply to a Comment"},{"location":"sp/comments-likes/#load-replies-to-a-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const replies = await comments[0].replies();","title":"Load Replies to a Comment"},{"location":"sp/comments-likes/#like","text":"You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/item\"; import { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\"; // like an item await item.like(); // unlike an item await item.unlike(); // get the liked by data const likedByData: ILikeData[] = await item.getLikedBy(); // get the liked by information const likedByInfo: ILikedByInformation = await item.getLikedByInformation(); To like/unlike a client-side page and get liked by information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/clientside-page\"; import { ILikedByInformation } from \"@pnp/sp/comments\"; // like a page await page.like(); // unlike a page await page.unlike(); // get the liked by information const likedByInfo: ILikedByInformation = await page.getLikedByInformation();","title":"Like"},{"location":"sp/content-types/","text":"@pnp/sp/content-types \u00b6 Content Types are used to define sets of columns in SharePoint. IContentTypes \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { ContentTypes, IContentTypes } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentTypes, IContentTypes } from \"@pnp/sp/presets/all\"; Add an existing Content Type to a collection \u00b6 The following example shows how to add the built in Picture Content Type to the Documents library. sp.web.lists.getByTitle(\"Documents\").contentTypes.addAvailableContentType(\"0x010102\"); Get a Content Type by Id \u00b6 const d: IContentType = await sp.web.contentTypes.getById(\"0x01\")(); // log content type name to console console.log(d.name); Add a new Content Type \u00b6 To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\"); It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). //Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings) sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\", \"This is my content type.\", \"_PnP Content Types\", { ReadOnly: true }); IContentType \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ContentType, IContentType } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentType, IContentType } from \"@pnp/sp/presets/all\"; Get the field links \u00b6 Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type. // get field links from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fieldLinks(); // log collection of fieldlinks to console console.log(d); Get Content Type fields \u00b6 To get a collection with all fields on the Content Type, simply use this method. // get fields from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fields(); // log collection of fields to console console.log(d); Get parent Content Type \u00b6 // get parent Content Type from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").parent(); // log name of parent Content Type to console console.log(d.Name) Get Content Type Workflow associations \u00b6 // get workflow associations from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").workflowAssociations(); // log collection of workflow associations to console console.log(d);","title":"Content Types"},{"location":"sp/content-types/#pnpspcontent-types","text":"Content Types are used to define sets of columns in SharePoint.","title":"@pnp/sp/content-types"},{"location":"sp/content-types/#icontenttypes","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { ContentTypes, IContentTypes } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentTypes, IContentTypes } from \"@pnp/sp/presets/all\";","title":"IContentTypes"},{"location":"sp/content-types/#add-an-existing-content-type-to-a-collection","text":"The following example shows how to add the built in Picture Content Type to the Documents library. sp.web.lists.getByTitle(\"Documents\").contentTypes.addAvailableContentType(\"0x010102\");","title":"Add an existing Content Type to a collection"},{"location":"sp/content-types/#get-a-content-type-by-id","text":"const d: IContentType = await sp.web.contentTypes.getById(\"0x01\")(); // log content type name to console console.log(d.name);","title":"Get a Content Type by Id"},{"location":"sp/content-types/#add-a-new-content-type","text":"To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\"); It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). //Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings) sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\", \"This is my content type.\", \"_PnP Content Types\", { ReadOnly: true });","title":"Add a new Content Type"},{"location":"sp/content-types/#icontenttype","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ContentType, IContentType } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentType, IContentType } from \"@pnp/sp/presets/all\";","title":"IContentType"},{"location":"sp/content-types/#get-the-field-links","text":"Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type. // get field links from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fieldLinks(); // log collection of fieldlinks to console console.log(d);","title":"Get the field links"},{"location":"sp/content-types/#get-content-type-fields","text":"To get a collection with all fields on the Content Type, simply use this method. // get fields from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fields(); // log collection of fields to console console.log(d);","title":"Get Content Type fields"},{"location":"sp/content-types/#get-parent-content-type","text":"// get parent Content Type from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").parent(); // log name of parent Content Type to console console.log(d.Name)","title":"Get parent Content Type"},{"location":"sp/content-types/#get-content-type-workflow-associations","text":"// get workflow associations from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").workflowAssociations(); // log collection of workflow associations to console console.log(d);","title":"Get Content Type Workflow associations"},{"location":"sp/custom-irequestclient/","text":"Custom IRequestClient \u00b6 Scenario: You have some special requirements involving auth scenarios or other needs that the library can't directly support. You may need to create a custom IRequestClient implementation to meet those needs as we can't customize the library to handle every case. This article walks you through how to create a custom IRequestClient and register it for use by the library. It is very unlikely this is a step you ever need to take and we encourage you to ask a question in the issues list before going down this path. Create the Client \u00b6 The easiest way to create a new IRequestClient is to subclass the existing SPHttpClient. You can always write a full client from scratch so long as it supports the IRequestClient interface but you need to handle all of the logic for retry, headers, and the request digest. Here we show implementing a client to solve the need discussed in pull request 1264 as an example. // we subclass SPHttpClient class CustomSPHttpClient extends SPHttpClient { // optionally add a constructor, done here as an example constructor(impl?: IHttpClientImpl) { super(impl); } // override the fetchRaw method to ensure we always include the credentials = \"include\" option // you could also override fetch, but fetchRaw ensures no matter what all requests get your custom logic is applied public fetchRaw(url: string, options?: IFetchOptions): Promise { options.credentials = \"include\"; return super.fetchRaw(url, options); } } The final step is to register the custom client with the library so it is used instead of the default. For that we import the registerCustomRequestClientFactory function and call it before our request generating code. You can reset to the default client factory by passing null to this same function. import { sp, registerCustomRequestClientFactory } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; registerCustomRequestClientFactory(() => new CustomSPHttpClient()); // configure your other options sp.setup({ // ... }); // this request will be executed through your custom client const w = await sp.web(); Unregister Custom Client \u00b6 // unregister custom client factory registerCustomRequestClientFactory(null); IRequestClient Interface \u00b6 If you want to 100% roll your own client you need to implement the below interface, found in common. import { IRequestClient } from \"@pnp/core\"; export interface IRequestClient { fetch(url: string, options?: IFetchOptions): Promise; fetchRaw(url: string, options?: IFetchOptions): Promise; get(url: string, options?: IFetchOptions): Promise; post(url: string, options?: IFetchOptions): Promise; patch(url: string, options?: IFetchOptions): Promise; delete(url: string, options?: IFetchOptions): Promise; } Supportability Note \u00b6 We cannot provide support for your custom client implementation, and creating your own client assumes an intimate knowledge of how SharePoint requests work. Again, this is very likely something you will never need to do - and we recommend exhausting all other options before taking this route.","title":"Custom Request Client"},{"location":"sp/custom-irequestclient/#custom-irequestclient","text":"Scenario: You have some special requirements involving auth scenarios or other needs that the library can't directly support. You may need to create a custom IRequestClient implementation to meet those needs as we can't customize the library to handle every case. This article walks you through how to create a custom IRequestClient and register it for use by the library. It is very unlikely this is a step you ever need to take and we encourage you to ask a question in the issues list before going down this path.","title":"Custom IRequestClient"},{"location":"sp/custom-irequestclient/#create-the-client","text":"The easiest way to create a new IRequestClient is to subclass the existing SPHttpClient. You can always write a full client from scratch so long as it supports the IRequestClient interface but you need to handle all of the logic for retry, headers, and the request digest. Here we show implementing a client to solve the need discussed in pull request 1264 as an example. // we subclass SPHttpClient class CustomSPHttpClient extends SPHttpClient { // optionally add a constructor, done here as an example constructor(impl?: IHttpClientImpl) { super(impl); } // override the fetchRaw method to ensure we always include the credentials = \"include\" option // you could also override fetch, but fetchRaw ensures no matter what all requests get your custom logic is applied public fetchRaw(url: string, options?: IFetchOptions): Promise { options.credentials = \"include\"; return super.fetchRaw(url, options); } } The final step is to register the custom client with the library so it is used instead of the default. For that we import the registerCustomRequestClientFactory function and call it before our request generating code. You can reset to the default client factory by passing null to this same function. import { sp, registerCustomRequestClientFactory } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; registerCustomRequestClientFactory(() => new CustomSPHttpClient()); // configure your other options sp.setup({ // ... }); // this request will be executed through your custom client const w = await sp.web();","title":"Create the Client"},{"location":"sp/custom-irequestclient/#unregister-custom-client","text":"// unregister custom client factory registerCustomRequestClientFactory(null);","title":"Unregister Custom Client"},{"location":"sp/custom-irequestclient/#irequestclient-interface","text":"If you want to 100% roll your own client you need to implement the below interface, found in common. import { IRequestClient } from \"@pnp/core\"; export interface IRequestClient { fetch(url: string, options?: IFetchOptions): Promise; fetchRaw(url: string, options?: IFetchOptions): Promise; get(url: string, options?: IFetchOptions): Promise; post(url: string, options?: IFetchOptions): Promise; patch(url: string, options?: IFetchOptions): Promise; delete(url: string, options?: IFetchOptions): Promise; }","title":"IRequestClient Interface"},{"location":"sp/custom-irequestclient/#supportability-note","text":"We cannot provide support for your custom client implementation, and creating your own client assumes an intimate knowledge of how SharePoint requests work. Again, this is very likely something you will never need to do - and we recommend exhausting all other options before taking this route.","title":"Supportability Note"},{"location":"sp/entity-merging/","text":"@pnp/sp - entity merging \u00b6 Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its representing type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples. Importing spODataEntity and spODataEntityArray \u00b6 You can import spODataEntity and spODataEntityArray in two ways, depending on your use case. The simplest way is to use the presets/all import as shown in the examples. The downside of this approach is that you can't take advantage of selective imports. If you want to take advantage of selective imports while using either of the entity parsers you can use: import { spODataEntity, spODataEntityArray } from \"@pnp/sp/odata\"; The full selective import for the first sample would be: import { sp } from \"@pnp/sp\"; import { spODataEntity } from \"@pnp/sp/odata\"; import { Item, IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; Request a single entity \u00b6 If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query. import { sp, spODataEntity, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; } try { // get a list item loaded with data and merged into an instance of Item const item = await sp.web.lists.getByTitle(\"ListTitle\").items.getById(1).usingParser(spODataEntity(Item))(); // log the item id, all properties specified in MyProps will be type checked Logger.write(`Item id: ${item.Id}`); // now we can call update because we have an instance of the Item type to work with as well await item.update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); } Request a collection \u00b6 The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method. import { sp, spODataEntityArray, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; Title: string; } try { // get a list item loaded with data and merged into an instance of Item const items = await sp.web.lists.getByTitle(\"OrderByList\").items.select(\"Id\", \"Title\").usingParser(spODataEntityArray(Item))(); Logger.write(`Item id: ${items.length}`); Logger.write(`Item id: ${items[0].Title}`); // now we can call update because we have an instance of the Item type to work with as well await items[0].update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); }","title":"Entity Merging"},{"location":"sp/entity-merging/#pnpsp-entity-merging","text":"Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its representing type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples.","title":"@pnp/sp - entity merging"},{"location":"sp/entity-merging/#importing-spodataentity-and-spodataentityarray","text":"You can import spODataEntity and spODataEntityArray in two ways, depending on your use case. The simplest way is to use the presets/all import as shown in the examples. The downside of this approach is that you can't take advantage of selective imports. If you want to take advantage of selective imports while using either of the entity parsers you can use: import { spODataEntity, spODataEntityArray } from \"@pnp/sp/odata\"; The full selective import for the first sample would be: import { sp } from \"@pnp/sp\"; import { spODataEntity } from \"@pnp/sp/odata\"; import { Item, IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\";","title":"Importing spODataEntity and spODataEntityArray"},{"location":"sp/entity-merging/#request-a-single-entity","text":"If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query. import { sp, spODataEntity, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; } try { // get a list item loaded with data and merged into an instance of Item const item = await sp.web.lists.getByTitle(\"ListTitle\").items.getById(1).usingParser(spODataEntity(Item))(); // log the item id, all properties specified in MyProps will be type checked Logger.write(`Item id: ${item.Id}`); // now we can call update because we have an instance of the Item type to work with as well await item.update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); }","title":"Request a single entity"},{"location":"sp/entity-merging/#request-a-collection","text":"The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method. import { sp, spODataEntityArray, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; Title: string; } try { // get a list item loaded with data and merged into an instance of Item const items = await sp.web.lists.getByTitle(\"OrderByList\").items.select(\"Id\", \"Title\").usingParser(spODataEntityArray(Item))(); Logger.write(`Item id: ${items.length}`); Logger.write(`Item id: ${items[0].Title}`); // now we can call update because we have an instance of the Item type to work with as well await items[0].update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); }","title":"Request a collection"},{"location":"sp/features/","text":"@pnp/sp/features \u00b6 Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web. IFeatures \u00b6 Represents a collection of features. SharePoint Sites and Webs will have a collection of features Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\"; getById \u00b6 Gets the information about a feature for the given GUID import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; const webFeature = await sp.web.features.getById(webFeatureId)(); const siteFeatureId = \"guid-of-site-scope-feature\"; const siteFeature = await sp.site.features.getById(siteFeatureId)(); add \u00b6 Adds (activates) a feature at the Site or Web level import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.add(webFeatureId); // Activate with force res = await sp.web.features.add(webFeatureId, true); remove \u00b6 Removes and deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.remove(webFeatureId); // Deactivate with force res = await sp.web.features.remove(webFeatureId, true); IFeature \u00b6 Represents an instance of a SharePoint feature. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features, IFeature, Feature } from \"@pnp/sp/presets/all\"; deactivate \u00b6 Deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; sp.web.features.getById(webFeatureId).deactivate() // Deactivate with force sp.web.features.getById(webFeatureId).deactivate(true)","title":"Features"},{"location":"sp/features/#pnpspfeatures","text":"Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.","title":"@pnp/sp/features"},{"location":"sp/features/#ifeatures","text":"Represents a collection of features. SharePoint Sites and Webs will have a collection of features Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\";","title":"IFeatures"},{"location":"sp/features/#getbyid","text":"Gets the information about a feature for the given GUID import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; const webFeature = await sp.web.features.getById(webFeatureId)(); const siteFeatureId = \"guid-of-site-scope-feature\"; const siteFeature = await sp.site.features.getById(siteFeatureId)();","title":"getById"},{"location":"sp/features/#add","text":"Adds (activates) a feature at the Site or Web level import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.add(webFeatureId); // Activate with force res = await sp.web.features.add(webFeatureId, true);","title":"add"},{"location":"sp/features/#remove","text":"Removes and deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.remove(webFeatureId); // Deactivate with force res = await sp.web.features.remove(webFeatureId, true);","title":"remove"},{"location":"sp/features/#ifeature","text":"Represents an instance of a SharePoint feature. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features, IFeature, Feature } from \"@pnp/sp/presets/all\";","title":"IFeature"},{"location":"sp/features/#deactivate","text":"Deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; sp.web.features.getById(webFeatureId).deactivate() // Deactivate with force sp.web.features.getById(webFeatureId).deactivate(true)","title":"deactivate"},{"location":"sp/fields/","text":"@pnp/sp/lists \u00b6 Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list. IFields \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Fields, IFields } from \"@pnp/sp/fields\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; Preset: All import { sp, Fields, IFields } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Fields, IFields } from \"@pnp/sp/presets/core\"; Get Field by Id \u00b6 Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/fields\"; // get the field by Id for web const field: IField = sp.web.fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // get the field by Id for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\")(); // we can use this 'field' variable to execute more queries on the field: const r = await field.select(\"Title\")(); // show the response from the server console.log(r.Title); Get Field by Title \u00b6 You can also get a field from the collection by title. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the title 'Author' for web const field: IField = sp.web.fields.getByTitle(\"Author\"); // get the field with the title 'Author' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Author\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Get Field by Internal Name or Title \u00b6 You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the internal name 'ModifiedBy' for web const field: IField = sp.web.fields.getByInternalNameOrTitle(\"ModifiedBy\"); // get the field with the internal name 'ModifiedBy' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByInternalNameOrTitle(\"ModifiedBy\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Create a Field using an XML schema \u00b6 Create a new field by defining an XML schema that assigns all the properties for the field. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // define the schema for your new field, in this case a date field with a default date of today. const fieldSchema = `[today]`; // create the new field in the web const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema); // create the new field in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(fieldSchema); // we can use this 'field' variable to run more queries on the list: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a New Field \u00b6 Use the add method to create a new field where you define the field type import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // create a new field called 'My Field' in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Site Field to a List \u00b6 Use the createFieldAsXml method to add a site field to a list. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // add the site field 'My Field' to the list 'My List' const r = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(field.data.SchemaXml); // log the field Id to console console.log(r.data.Id); Add a Text Field \u00b6 Use the addText method to create a new text field. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new text field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // create a new text field called 'My Field' in the list 'My List'. const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Calculated Field \u00b6 Use the addCalculated method to create a new calculated field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, FieldTypes } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new calculated field called 'My Field' in web const field = await sp.web.fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // create a new calculated field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Date/Time Field \u00b6 Use the addDateTime method to create a new date/time field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new date/time field called 'My Field' in web const field = await sp.web.fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // create a new date/time field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Currency Field \u00b6 Use the addCurrency method to create a new currency field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new currency field called 'My Field' in web const field = await sp.web.fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // create a new currency field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Multi-line Text Field \u00b6 Use the addMultilineText method to create a new multi-line text field. For Enhanced Rich Text mode, see the next section. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new multi-line text field called 'My Field' in web const field = await sp.web.fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // create a new multi-line text field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Multi-line Text Field with Enhanced Rich Text \u00b6 The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; //Create a new multi-line text field called 'My Field' in web const field = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml( `` ); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Number Field \u00b6 Use the addNumber method to create a new number field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new number field called 'My Field' in web const field = await sp.web.fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // create a new number field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a URL Field \u00b6 Use the addUrl method to create a new url field. import { sp } from \"@pnp/sp\"; import { UrlFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new url field called 'My Field' in web const field = await sp.web.fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // create a new url field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a User Field \u00b6 Use the addUser method to create a new user field. import { sp } from \"@pnp/sp\"; import { FieldUserSelectionMode } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new user field called 'My Field' in web const field = await sp.web.fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // create a new user field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Lookup Field \u00b6 Use the addLookup method to create a new lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const list = await sp.web.lists.getByTitle(\"My Lookup List\")(); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web. const field = await sp.web.fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); // ** // Adding a lookup that supports multiple values takes two calls: const fieldAddResult = await sp.web.fields.addLookup(\"Test Lookup 124\", \"GUID\", \"Title\"); await fieldAddResult.field.update({ Description: 'New Description' }, \"SP.FieldLookup\"); Add a Choice Field \u00b6 Use the addChoice method to create a new choice field. import { sp } from \"@pnp/sp\"; import { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new choice field called 'My Field' in web const field = await sp.web.fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // create a new choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Multi-Choice Field \u00b6 Use the addMultiChoice method to create a new multi-choice field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new multi-choice field called 'My Field' in web const field = await sp.web.fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // create a new multi-choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Boolean Field \u00b6 Use the addBoolean method to create a new boolean field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new boolean field called 'My Field' in web const field = await sp.web.fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // create a new boolean field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Dependent Lookup Field \u00b6 Use the addDependentLookupField method to create a new dependent lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web. const field = await sp.web.fields.getByTitle(\"My Field\")(); const fieldDep = await sp.web.fields.addDependentLookupField(\"My Dep Field\", field.Id, \"Description\"); // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\")(); const fieldDep2 = await sp.web.lists.getByTitle(\"My List\").fields.addDependentLookupField(\"My Dep Field\", field2.Id, \"Description\"); // we can use this 'fieldDep' variable to run more queries on the field: const r = await fieldDep.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Location Field \u00b6 Use the addLocation method to create a new location field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new location field called 'My Field' in web const field = await sp.web.fields.addLocation(\"My Field\", { Group: \"My Group\" }); // create a new location field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLocation(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Delete a Field \u00b6 Use the delete method to delete a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; // delete one or more fields from web, returns boolean const result = await sp.web.fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.fields.getByTitle(\"My Field 2\").delete(); // delete one or more fields from list 'My List', returns boolean const result = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field 2\").delete(); Update a Field \u00b6 Use the update method to update a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // update the field called 'My Field' with a description in web, returns FieldUpdateResult const fieldUpdate = await sp.web.fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // if you need to update a field with properties for a specific field type you can optionally include the field type as a second param // if you do not include it we will look up the type, but that adds a call to the server const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Look up Field\").update({ RelationshipDeleteBehavior: 1 }, \"SP.FieldLookup\"); Show a Field in the Display Form \u00b6 Use the setShowInDisplayForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in display form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInDisplayForm(true); // show field called 'My Field' in display form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInDisplayForm(true); Show a Field in the Edit Form \u00b6 Use the setShowInEditForm method to add a field to the edit form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in edit form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInEditForm(true); // show field called 'My Field' in edit form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInEditForm(true); Show a Field in the New Form \u00b6 Use the setShowInNewForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in new form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInNewForm(true); // show field called 'My Field' in new form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInNewForm(true);","title":"Fields"},{"location":"sp/fields/#pnpsplists","text":"Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.","title":"@pnp/sp/lists"},{"location":"sp/fields/#ifields","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Fields, IFields } from \"@pnp/sp/fields\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; Preset: All import { sp, Fields, IFields } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Fields, IFields } from \"@pnp/sp/presets/core\";","title":"IFields"},{"location":"sp/fields/#get-field-by-id","text":"Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/fields\"; // get the field by Id for web const field: IField = sp.web.fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // get the field by Id for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\")(); // we can use this 'field' variable to execute more queries on the field: const r = await field.select(\"Title\")(); // show the response from the server console.log(r.Title);","title":"Get Field by Id"},{"location":"sp/fields/#get-field-by-title","text":"You can also get a field from the collection by title. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the title 'Author' for web const field: IField = sp.web.fields.getByTitle(\"Author\"); // get the field with the title 'Author' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Author\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Get Field by Title"},{"location":"sp/fields/#get-field-by-internal-name-or-title","text":"You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the internal name 'ModifiedBy' for web const field: IField = sp.web.fields.getByInternalNameOrTitle(\"ModifiedBy\"); // get the field with the internal name 'ModifiedBy' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByInternalNameOrTitle(\"ModifiedBy\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Get Field by Internal Name or Title"},{"location":"sp/fields/#create-a-field-using-an-xml-schema","text":"Create a new field by defining an XML schema that assigns all the properties for the field. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // define the schema for your new field, in this case a date field with a default date of today. const fieldSchema = `[today]`; // create the new field in the web const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema); // create the new field in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(fieldSchema); // we can use this 'field' variable to run more queries on the list: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Create a Field using an XML schema"},{"location":"sp/fields/#add-a-new-field","text":"Use the add method to create a new field where you define the field type import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // create a new field called 'My Field' in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a New Field"},{"location":"sp/fields/#add-a-site-field-to-a-list","text":"Use the createFieldAsXml method to add a site field to a list. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // add the site field 'My Field' to the list 'My List' const r = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(field.data.SchemaXml); // log the field Id to console console.log(r.data.Id);","title":"Add a Site Field to a List"},{"location":"sp/fields/#add-a-text-field","text":"Use the addText method to create a new text field. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new text field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // create a new text field called 'My Field' in the list 'My List'. const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Text Field"},{"location":"sp/fields/#add-a-calculated-field","text":"Use the addCalculated method to create a new calculated field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, FieldTypes } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new calculated field called 'My Field' in web const field = await sp.web.fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // create a new calculated field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Calculated Field"},{"location":"sp/fields/#add-a-datetime-field","text":"Use the addDateTime method to create a new date/time field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new date/time field called 'My Field' in web const field = await sp.web.fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // create a new date/time field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Date/Time Field"},{"location":"sp/fields/#add-a-currency-field","text":"Use the addCurrency method to create a new currency field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new currency field called 'My Field' in web const field = await sp.web.fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // create a new currency field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Currency Field"},{"location":"sp/fields/#add-a-multi-line-text-field","text":"Use the addMultilineText method to create a new multi-line text field. For Enhanced Rich Text mode, see the next section. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new multi-line text field called 'My Field' in web const field = await sp.web.fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // create a new multi-line text field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Multi-line Text Field"},{"location":"sp/fields/#add-a-multi-line-text-field-with-enhanced-rich-text","text":"The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; //Create a new multi-line text field called 'My Field' in web const field = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml( `` ); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Multi-line Text Field with Enhanced Rich Text"},{"location":"sp/fields/#add-a-number-field","text":"Use the addNumber method to create a new number field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new number field called 'My Field' in web const field = await sp.web.fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // create a new number field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Number Field"},{"location":"sp/fields/#add-a-url-field","text":"Use the addUrl method to create a new url field. import { sp } from \"@pnp/sp\"; import { UrlFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new url field called 'My Field' in web const field = await sp.web.fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // create a new url field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a URL Field"},{"location":"sp/fields/#add-a-user-field","text":"Use the addUser method to create a new user field. import { sp } from \"@pnp/sp\"; import { FieldUserSelectionMode } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new user field called 'My Field' in web const field = await sp.web.fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // create a new user field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a User Field"},{"location":"sp/fields/#add-a-lookup-field","text":"Use the addLookup method to create a new lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const list = await sp.web.lists.getByTitle(\"My Lookup List\")(); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web. const field = await sp.web.fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); // ** // Adding a lookup that supports multiple values takes two calls: const fieldAddResult = await sp.web.fields.addLookup(\"Test Lookup 124\", \"GUID\", \"Title\"); await fieldAddResult.field.update({ Description: 'New Description' }, \"SP.FieldLookup\");","title":"Add a Lookup Field"},{"location":"sp/fields/#add-a-choice-field","text":"Use the addChoice method to create a new choice field. import { sp } from \"@pnp/sp\"; import { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new choice field called 'My Field' in web const field = await sp.web.fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // create a new choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Choice Field"},{"location":"sp/fields/#add-a-multi-choice-field","text":"Use the addMultiChoice method to create a new multi-choice field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new multi-choice field called 'My Field' in web const field = await sp.web.fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // create a new multi-choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Multi-Choice Field"},{"location":"sp/fields/#add-a-boolean-field","text":"Use the addBoolean method to create a new boolean field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new boolean field called 'My Field' in web const field = await sp.web.fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // create a new boolean field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Boolean Field"},{"location":"sp/fields/#add-a-dependent-lookup-field","text":"Use the addDependentLookupField method to create a new dependent lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web. const field = await sp.web.fields.getByTitle(\"My Field\")(); const fieldDep = await sp.web.fields.addDependentLookupField(\"My Dep Field\", field.Id, \"Description\"); // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\")(); const fieldDep2 = await sp.web.lists.getByTitle(\"My List\").fields.addDependentLookupField(\"My Dep Field\", field2.Id, \"Description\"); // we can use this 'fieldDep' variable to run more queries on the field: const r = await fieldDep.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Dependent Lookup Field"},{"location":"sp/fields/#add-a-location-field","text":"Use the addLocation method to create a new location field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new location field called 'My Field' in web const field = await sp.web.fields.addLocation(\"My Field\", { Group: \"My Group\" }); // create a new location field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLocation(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Location Field"},{"location":"sp/fields/#delete-a-field","text":"Use the delete method to delete a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; // delete one or more fields from web, returns boolean const result = await sp.web.fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.fields.getByTitle(\"My Field 2\").delete(); // delete one or more fields from list 'My List', returns boolean const result = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field 2\").delete();","title":"Delete a Field"},{"location":"sp/fields/#update-a-field","text":"Use the update method to update a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // update the field called 'My Field' with a description in web, returns FieldUpdateResult const fieldUpdate = await sp.web.fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // if you need to update a field with properties for a specific field type you can optionally include the field type as a second param // if you do not include it we will look up the type, but that adds a call to the server const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Look up Field\").update({ RelationshipDeleteBehavior: 1 }, \"SP.FieldLookup\");","title":"Update a Field"},{"location":"sp/fields/#show-a-field-in-the-display-form","text":"Use the setShowInDisplayForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in display form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInDisplayForm(true); // show field called 'My Field' in display form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInDisplayForm(true);","title":"Show a Field in the Display Form"},{"location":"sp/fields/#show-a-field-in-the-edit-form","text":"Use the setShowInEditForm method to add a field to the edit form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in edit form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInEditForm(true); // show field called 'My Field' in edit form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInEditForm(true);","title":"Show a Field in the Edit Form"},{"location":"sp/fields/#show-a-field-in-the-new-form","text":"Use the setShowInNewForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in new form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInNewForm(true); // show field called 'My Field' in new form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInNewForm(true);","title":"Show a Field in the New Form"},{"location":"sp/files/","text":"@pnp/sp/files \u00b6 One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below. Reading Files \u00b6 Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const blob: Blob = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBlob(); const buffer: ArrayBuffer = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBuffer(); const json: any = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.json\").getJSON(); const text: string = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.txt\").getText(); // all of these also work from a file object no matter how you access it const text2: string = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/documents\").files.getByName(\"file.txt\").getText(); getFileByUrl \u00b6 Added in 2.0.4 This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const url = \"{absolute file url OR sharing url}\"; // file is an IFile and supports all the file operations const file = sp.web.getFileByUrl(url); // for example const fileContent = await file.getText(); Adding Files \u00b6 Likewise you can add files using one of two methods, add or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. declare var require: (s: string) => any; import { ConsoleListener, Logger, LogLevel } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import { auth } from \"./auth\"; let $ = require(\"jquery\"); // <-- used here for illustration let siteUrl = \"https://mytenant.sharepoint.com/sites/dev\"; // comment this out for non-node execution // auth(siteUrl); Logger.subscribe(new ConsoleListener()); Logger.activeLogLevel = LogLevel.Verbose; let web = Web(siteUrl); $(() => { $(\"#testingdiv\").append(\"\"); $(\"#thebuttontodoit\").on('click', async (e) => { e.preventDefault(); let input = document.getElementById(\"thefileinput\"); let file = input.files[0]; // you can adjust this number to control what size files are uploaded in chunks if (file.size <= 10485760) { // small upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(file.name, file, true); Logger.write(\"done\"); } else { // large upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addChunked(file.name, file, data => { Logger.log({ data: data, level: LogLevel.Verbose, message: \"progress\" }); }, true); Logger.write(\"done!\") } }); }); Adding a file using Nodejs Streams \u00b6 If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams. // triggers auto-application of extensions, in this case to add getStream import \"@pnp/nodejs\"; // get a stream of an existing file const sr = await sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/old.md\").getStream(); // now add the stream as a new file, remember to set the content-length header const fr = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.configure({ headers: { \"content-length\": `${sr.knownLength}`, }, }).add(\"new.md\", sr.body); Setting Associated Item Values \u00b6 You can also update the file properties of a newly uploaded file using code similar to the below snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(\"file.name\", \"file\", true); const item = await file.file.getItem(); await item.update({ Title: \"A Title\", OtherField: \"My Other Value\" }); AddUsingPath \u00b6 If you need to support the percent or pound characters you can use the addUsingPath method of IFiles import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addUsingPath(\"file%#%.name\", \"content\"); Update File Content \u00b6 You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.txt\").setContent(\"New string content for the file.\"); await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.mp4\").setContentChunked(file); Check in, Check out, and Approve & Deny \u00b6 The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below. Check In \u00b6 Check in takes two optional arguments, comment and check in type. import { sp } from \"@pnp/sp\"; import { CheckinType } from \"@pnp/sp/files\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // default options with empty comment and CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(); console.log(\"File checked in!\"); // supply a comment (< 1024 chars) and using default check in type CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\"); console.log(\"File checked in!\"); // Supply both comment and check in type await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\", CheckinType.Overwrite); console.log(\"File checked in!\"); Check Out \u00b6 Check out takes no arguments. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkout(); console.log(\"File checked out!\"); Approve and Deny \u00b6 You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").approve(\"Approval Comment\"); console.log(\"File approved!\"); // deny with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(); console.log(\"File denied!\"); // deny with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(\"Deny comment\"); console.log(\"File denied!\"); Publish and Unpublish \u00b6 You can both publish and unpublish a file using the library. Both methods take an optional comment argument. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // publish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(); console.log(\"File published!\"); // publish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(\"Publish comment\"); console.log(\"File published!\"); // unpublish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(); console.log(\"File unpublished!\"); // unpublish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(\"Unpublish comment\"); console.log(\"File unpublished!\"); Advanced Upload Options \u00b6 Both the addChunked and setContentChunked methods support options beyond just supplying the file content. progress function \u00b6 A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: (data: ChunkedFileUploadProgressData) => void The data interface is: export interface ChunkedFileUploadProgressData { stage: \"starting\" | \"continue\" | \"finishing\"; blockNumber: number; totalBlocks: number; chunkSize: number; currentPointer: number; fileSize: number; } chunkSize \u00b6 This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts. getItem \u00b6 This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/security\"; const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(); console.log(item); const item2 = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(\"Title\", \"Modified\"); console.log(item2); // you can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms); You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/items\"; import \"@pnp/sp/security\"; // also supports typing the objects so your type will be a union type const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem<{ Id: number, Title: string }>(\"Id\", \"Title\"); // You get intellisense and proper typing of the returned object console.log(`Id: ${item.Id} -- ${item.Title}`); // You can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms); move \u00b6 It's possible to move a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveTo(destinationUrl); copy \u00b6 It's possible to copy a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyTo(destinationUrl, false); move by path \u00b6 It's possible to move a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveByPath(destinationUrl, false, true); copy by path \u00b6 It's possible to copy a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, true); getFileById \u00b6 You can get a file by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import { IFile } from \"@pnp/sp/files\"; const file: IFile = sp.web.getFileById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); delete \u00b6 Deletes a file import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").delete(); delete with params \u00b6 Added in 2.0.9 Deletes a file with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").deleteWithParams({ BypassSharedLock: true, }); exists \u00b6 Added in 2.0.9 Checks to see if a file exists import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; const exists = await sp.web.rootFolder.files.getByName(\"name.txt\").exists();","title":"Files"},{"location":"sp/files/#pnpspfiles","text":"One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.","title":"@pnp/sp/files"},{"location":"sp/files/#reading-files","text":"Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const blob: Blob = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBlob(); const buffer: ArrayBuffer = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBuffer(); const json: any = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.json\").getJSON(); const text: string = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.txt\").getText(); // all of these also work from a file object no matter how you access it const text2: string = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/documents\").files.getByName(\"file.txt\").getText();","title":"Reading Files"},{"location":"sp/files/#getfilebyurl","text":"Added in 2.0.4 This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const url = \"{absolute file url OR sharing url}\"; // file is an IFile and supports all the file operations const file = sp.web.getFileByUrl(url); // for example const fileContent = await file.getText();","title":"getFileByUrl"},{"location":"sp/files/#adding-files","text":"Likewise you can add files using one of two methods, add or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. declare var require: (s: string) => any; import { ConsoleListener, Logger, LogLevel } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import { auth } from \"./auth\"; let $ = require(\"jquery\"); // <-- used here for illustration let siteUrl = \"https://mytenant.sharepoint.com/sites/dev\"; // comment this out for non-node execution // auth(siteUrl); Logger.subscribe(new ConsoleListener()); Logger.activeLogLevel = LogLevel.Verbose; let web = Web(siteUrl); $(() => { $(\"#testingdiv\").append(\"\"); $(\"#thebuttontodoit\").on('click', async (e) => { e.preventDefault(); let input = document.getElementById(\"thefileinput\"); let file = input.files[0]; // you can adjust this number to control what size files are uploaded in chunks if (file.size <= 10485760) { // small upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(file.name, file, true); Logger.write(\"done\"); } else { // large upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addChunked(file.name, file, data => { Logger.log({ data: data, level: LogLevel.Verbose, message: \"progress\" }); }, true); Logger.write(\"done!\") } }); });","title":"Adding Files"},{"location":"sp/files/#adding-a-file-using-nodejs-streams","text":"If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams. // triggers auto-application of extensions, in this case to add getStream import \"@pnp/nodejs\"; // get a stream of an existing file const sr = await sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/old.md\").getStream(); // now add the stream as a new file, remember to set the content-length header const fr = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.configure({ headers: { \"content-length\": `${sr.knownLength}`, }, }).add(\"new.md\", sr.body);","title":"Adding a file using Nodejs Streams"},{"location":"sp/files/#setting-associated-item-values","text":"You can also update the file properties of a newly uploaded file using code similar to the below snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(\"file.name\", \"file\", true); const item = await file.file.getItem(); await item.update({ Title: \"A Title\", OtherField: \"My Other Value\" });","title":"Setting Associated Item Values"},{"location":"sp/files/#addusingpath","text":"If you need to support the percent or pound characters you can use the addUsingPath method of IFiles import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addUsingPath(\"file%#%.name\", \"content\");","title":"AddUsingPath"},{"location":"sp/files/#update-file-content","text":"You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.txt\").setContent(\"New string content for the file.\"); await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.mp4\").setContentChunked(file);","title":"Update File Content"},{"location":"sp/files/#check-in-check-out-and-approve-deny","text":"The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.","title":"Check in, Check out, and Approve & Deny"},{"location":"sp/files/#check-in","text":"Check in takes two optional arguments, comment and check in type. import { sp } from \"@pnp/sp\"; import { CheckinType } from \"@pnp/sp/files\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // default options with empty comment and CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(); console.log(\"File checked in!\"); // supply a comment (< 1024 chars) and using default check in type CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\"); console.log(\"File checked in!\"); // Supply both comment and check in type await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\", CheckinType.Overwrite); console.log(\"File checked in!\");","title":"Check In"},{"location":"sp/files/#check-out","text":"Check out takes no arguments. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkout(); console.log(\"File checked out!\");","title":"Check Out"},{"location":"sp/files/#approve-and-deny","text":"You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").approve(\"Approval Comment\"); console.log(\"File approved!\"); // deny with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(); console.log(\"File denied!\"); // deny with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(\"Deny comment\"); console.log(\"File denied!\");","title":"Approve and Deny"},{"location":"sp/files/#publish-and-unpublish","text":"You can both publish and unpublish a file using the library. Both methods take an optional comment argument. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // publish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(); console.log(\"File published!\"); // publish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(\"Publish comment\"); console.log(\"File published!\"); // unpublish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(); console.log(\"File unpublished!\"); // unpublish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(\"Unpublish comment\"); console.log(\"File unpublished!\");","title":"Publish and Unpublish"},{"location":"sp/files/#advanced-upload-options","text":"Both the addChunked and setContentChunked methods support options beyond just supplying the file content.","title":"Advanced Upload Options"},{"location":"sp/files/#progress-function","text":"A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: (data: ChunkedFileUploadProgressData) => void The data interface is: export interface ChunkedFileUploadProgressData { stage: \"starting\" | \"continue\" | \"finishing\"; blockNumber: number; totalBlocks: number; chunkSize: number; currentPointer: number; fileSize: number; }","title":"progress function"},{"location":"sp/files/#chunksize","text":"This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.","title":"chunkSize"},{"location":"sp/files/#getitem","text":"This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/security\"; const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(); console.log(item); const item2 = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(\"Title\", \"Modified\"); console.log(item2); // you can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms); You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/items\"; import \"@pnp/sp/security\"; // also supports typing the objects so your type will be a union type const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem<{ Id: number, Title: string }>(\"Id\", \"Title\"); // You get intellisense and proper typing of the returned object console.log(`Id: ${item.Id} -- ${item.Title}`); // You can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms);","title":"getItem"},{"location":"sp/files/#move","text":"It's possible to move a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveTo(destinationUrl);","title":"move"},{"location":"sp/files/#copy","text":"It's possible to copy a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyTo(destinationUrl, false);","title":"copy"},{"location":"sp/files/#move-by-path","text":"It's possible to move a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveByPath(destinationUrl, false, true);","title":"move by path"},{"location":"sp/files/#copy-by-path","text":"It's possible to copy a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, true);","title":"copy by path"},{"location":"sp/files/#getfilebyid","text":"You can get a file by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import { IFile } from \"@pnp/sp/files\"; const file: IFile = sp.web.getFileById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");","title":"getFileById"},{"location":"sp/files/#delete","text":"Deletes a file import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").delete();","title":"delete"},{"location":"sp/files/#delete-with-params","text":"Added in 2.0.9 Deletes a file with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").deleteWithParams({ BypassSharedLock: true, });","title":"delete with params"},{"location":"sp/files/#exists","text":"Added in 2.0.9 Checks to see if a file exists import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; const exists = await sp.web.rootFolder.files.getByName(\"name.txt\").exists();","title":"exists"},{"location":"sp/folders/","text":"@pnp/sp/folders \u00b6 Folders serve as a container for your files and list items. IFolders \u00b6 Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\"; Get folders collection for various SharePoint objects \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; // gets web's folders const webFolders = await sp.web.folders(); // gets list's folders const listFolders = await sp.web.lists.getByTitle(\"My List\").rootFolder.folders(); // gets item's folders const itemFolders = await sp.web.lists.getByTitle(\"My List\").items.getById(1).folder.folders(); add \u00b6 Adds a new folder to collection of folders import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // creates a new folder for web with specified url const folderAddResult = await sp.web.folders.add(\"folder url\"); getByName \u00b6 Gets a folder instance from a collection by folder's name import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = await sp.web.folders.getByName(\"folder name\")(); IFolder \u00b6 Represents an instance of a SharePoint folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\"; Get a folder object associated with different SharePoint artifacts (web, list, list item) \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // web's folder const rootFolder = await sp.web.rootFolder(); // list's folder const listRootFolder = await sp.web.lists.getByTitle(\"234\").rootFolder(); // item's folder const itemFolder = await sp.web.lists.getByTitle(\"234\").items.getById(1).folder(); getItem \u00b6 Gets list item associated with a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folderItem = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").getItem(); move \u00b6 It's possible to move a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveTo(destinationUrl); copy \u00b6 It's possible to copy a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyTo(destinationUrl); move by path \u00b6 It's possible to move a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveByPath(destinationUrl, true); copy by path \u00b6 It's possible to copy a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyByPath(destinationUrl, true); delete \u00b6 Deletes a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").delete(); delete with params \u00b6 Added in 2.0.9 Deletes a folder with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").deleteWithParams({ BypassSharedLock: true, DeleteIfEmpty: true, }); recycle \u00b6 Recycles a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").recycle(); serverRelativeUrl \u00b6 Gets folder's server relative url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const relUrl = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").serverRelativeUrl(); update \u00b6 Updates folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").update({ \"Name\": \"New name\", }); contentTypeOrder \u00b6 Gets content type order of a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const order = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").contentTypeOrder(); folders \u00b6 Gets all child folders associated with the current folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folders = await sp.web.rootFolder.folders(); files \u00b6 Gets all files inside a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files/folder\"; const files = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files(); listItemAllFields \u00b6 Gets this folder's list item field values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const itemFields = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").listItemAllFields(); parentFolder \u00b6 Gets the parent folder, if available import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const parentFolder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").parentFolder(); properties \u00b6 Gets this folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const properties = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").properties(); uniqueContentTypeOrder \u00b6 Gets a value that specifies the content type order. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const contentTypeOrder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").uniqueContentTypeOrder(); Rename a folder \u00b6 You can rename a folder by updating FileLeafRef property: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\"); const item = await folder.getItem(); const result = await item.update({ FileLeafRef: \"Folder2\" }); Create a folder with custom content type \u00b6 Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; const newFolderResult = await sp.web.rootFolder.folders.getByName(\"Shared Documents\").folders.add(\"My New Folder\"); const item = await newFolderResult.folder.listItemAllFields(); await sp.web.lists.getByTitle(\"Documents\").items.getById(item.ID).update({ ContentTypeId: \"0x0120001E76ED75A3E3F3408811F0BF56C4CDDD\", MyFolderField: \"field value\", Title: \"My New Folder\", }); addSubFolderUsingPath \u00b6 Added in 2.0.9 You can use the addSubFolderUsingPath method to add a folder with some special chars supported import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; // add a folder to site assets const folder: IFolder = await web.rootFolder.folders.getByName(\"SiteAssets\").addSubFolderUsingPath(\"folder name\"); getFolderById \u00b6 You can get a folder by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); getParentInfos \u00b6 Added in 2.0.12 Gets information about folder, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); await folder.getParentInfos();","title":"Folders"},{"location":"sp/folders/#pnpspfolders","text":"Folders serve as a container for your files and list items.","title":"@pnp/sp/folders"},{"location":"sp/folders/#ifolders","text":"Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\";","title":"IFolders"},{"location":"sp/folders/#get-folders-collection-for-various-sharepoint-objects","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; // gets web's folders const webFolders = await sp.web.folders(); // gets list's folders const listFolders = await sp.web.lists.getByTitle(\"My List\").rootFolder.folders(); // gets item's folders const itemFolders = await sp.web.lists.getByTitle(\"My List\").items.getById(1).folder.folders();","title":"Get folders collection for various SharePoint objects"},{"location":"sp/folders/#add","text":"Adds a new folder to collection of folders import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // creates a new folder for web with specified url const folderAddResult = await sp.web.folders.add(\"folder url\");","title":"add"},{"location":"sp/folders/#getbyname","text":"Gets a folder instance from a collection by folder's name import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = await sp.web.folders.getByName(\"folder name\")();","title":"getByName"},{"location":"sp/folders/#ifolder","text":"Represents an instance of a SharePoint folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\";","title":"IFolder"},{"location":"sp/folders/#get-a-folder-object-associated-with-different-sharepoint-artifacts-web-list-list-item","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // web's folder const rootFolder = await sp.web.rootFolder(); // list's folder const listRootFolder = await sp.web.lists.getByTitle(\"234\").rootFolder(); // item's folder const itemFolder = await sp.web.lists.getByTitle(\"234\").items.getById(1).folder();","title":"Get a folder object associated with different SharePoint artifacts (web, list, list item)"},{"location":"sp/folders/#getitem","text":"Gets list item associated with a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folderItem = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").getItem();","title":"getItem"},{"location":"sp/folders/#move","text":"It's possible to move a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveTo(destinationUrl);","title":"move"},{"location":"sp/folders/#copy","text":"It's possible to copy a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyTo(destinationUrl);","title":"copy"},{"location":"sp/folders/#move-by-path","text":"It's possible to move a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveByPath(destinationUrl, true);","title":"move by path"},{"location":"sp/folders/#copy-by-path","text":"It's possible to copy a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyByPath(destinationUrl, true);","title":"copy by path"},{"location":"sp/folders/#delete","text":"Deletes a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").delete();","title":"delete"},{"location":"sp/folders/#delete-with-params","text":"Added in 2.0.9 Deletes a folder with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").deleteWithParams({ BypassSharedLock: true, DeleteIfEmpty: true, });","title":"delete with params"},{"location":"sp/folders/#recycle","text":"Recycles a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").recycle();","title":"recycle"},{"location":"sp/folders/#serverrelativeurl","text":"Gets folder's server relative url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const relUrl = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").serverRelativeUrl();","title":"serverRelativeUrl"},{"location":"sp/folders/#update","text":"Updates folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").update({ \"Name\": \"New name\", });","title":"update"},{"location":"sp/folders/#contenttypeorder","text":"Gets content type order of a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const order = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").contentTypeOrder();","title":"contentTypeOrder"},{"location":"sp/folders/#folders","text":"Gets all child folders associated with the current folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folders = await sp.web.rootFolder.folders();","title":"folders"},{"location":"sp/folders/#files","text":"Gets all files inside a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files/folder\"; const files = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files();","title":"files"},{"location":"sp/folders/#listitemallfields","text":"Gets this folder's list item field values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const itemFields = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").listItemAllFields();","title":"listItemAllFields"},{"location":"sp/folders/#parentfolder","text":"Gets the parent folder, if available import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const parentFolder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").parentFolder();","title":"parentFolder"},{"location":"sp/folders/#properties","text":"Gets this folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const properties = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").properties();","title":"properties"},{"location":"sp/folders/#uniquecontenttypeorder","text":"Gets a value that specifies the content type order. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const contentTypeOrder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").uniqueContentTypeOrder();","title":"uniqueContentTypeOrder"},{"location":"sp/folders/#rename-a-folder","text":"You can rename a folder by updating FileLeafRef property: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\"); const item = await folder.getItem(); const result = await item.update({ FileLeafRef: \"Folder2\" });","title":"Rename a folder"},{"location":"sp/folders/#create-a-folder-with-custom-content-type","text":"Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; const newFolderResult = await sp.web.rootFolder.folders.getByName(\"Shared Documents\").folders.add(\"My New Folder\"); const item = await newFolderResult.folder.listItemAllFields(); await sp.web.lists.getByTitle(\"Documents\").items.getById(item.ID).update({ ContentTypeId: \"0x0120001E76ED75A3E3F3408811F0BF56C4CDDD\", MyFolderField: \"field value\", Title: \"My New Folder\", });","title":"Create a folder with custom content type"},{"location":"sp/folders/#addsubfolderusingpath","text":"Added in 2.0.9 You can use the addSubFolderUsingPath method to add a folder with some special chars supported import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; // add a folder to site assets const folder: IFolder = await web.rootFolder.folders.getByName(\"SiteAssets\").addSubFolderUsingPath(\"folder name\");","title":"addSubFolderUsingPath"},{"location":"sp/folders/#getfolderbyid","text":"You can get a folder by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");","title":"getFolderById"},{"location":"sp/folders/#getparentinfos","text":"Added in 2.0.12 Gets information about folder, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); await folder.getParentInfos();","title":"getParentInfos"},{"location":"sp/forms/","text":"@pnp/sp/forms \u00b6 Forms in SharePoint are the Display, New, and Edit forms associated with a list. IFields \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\"; Get Form by Id \u00b6 Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\"; // get the field by Id for web const form = sp.web.lists.getByTitle(\"Documents\").forms.getById(\"{c4486774-f1e2-4804-96f3-91edf3e22a19}\")();","title":"Forms"},{"location":"sp/forms/#pnpspforms","text":"Forms in SharePoint are the Display, New, and Edit forms associated with a list.","title":"@pnp/sp/forms"},{"location":"sp/forms/#ifields","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\";","title":"IFields"},{"location":"sp/forms/#get-form-by-id","text":"Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\"; // get the field by Id for web const form = sp.web.lists.getByTitle(\"Documents\").forms.getById(\"{c4486774-f1e2-4804-96f3-91edf3e22a19}\")();","title":"Get Form by Id"},{"location":"sp/hubsites/","text":"@pnp/sp/hubsites \u00b6 This module helps you with working with hub sites in your tenant. IHubSites \u00b6 Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/hubsites\"; Preset: All import { sp, HubSites, IHubSites } from \"@pnp/sp/presets/all\"; Get a Listing of All Hub sites \u00b6 import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; // invoke the hub sites object const hubsites: IHubSiteInfo[] = await sp.hubSites(); // you can also use select to only return certain fields: const hubsites2: IHubSiteInfo[] = await sp.hubSites.select(\"ID\", \"Title\", \"RelatedHubSiteIds\")(); Get Hub site by Id \u00b6 Using the getById method on the hubsites module to get a hub site by site Id (guid). import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; const hubsite: IHubSiteInfo = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\")(); // log hub site title to console console.log(hubsite.Title); Get ISite instance \u00b6 We provide a helper method to load the ISite instance from the HubSite import { sp } from \"@pnp/sp\"; import { ISite } from \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites\"; const site: ISite = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\").getSite(); const siteData = await site(); console.log(siteData.Title); Get Hub site data for a web \u00b6 import { sp } from \"@pnp/sp\"; import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; const webData: Partial = await sp.web.hubSiteData(); // you can also force a refresh of the hub site data const webData2: Partial = await sp.web.hubSiteData(true); syncHubSiteTheme \u00b6 Allows you to apply theme updates from the parent hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; await sp.web.syncHubSiteTheme(); Hub site Site Methods \u00b6 You manage hub sites at the Site level. joinHubSite \u00b6 Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // join a site to a hub site await sp.site.joinHubSite(\"{parent hub site id}\"); // remove a site from a hub site await sp.site.joinHubSite(\"00000000-0000-0000-0000-000000000000\"); registerHubSite \u00b6 Registers the current site collection as hub site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // register current site as a hub site await sp.site.registerHubSite(); unRegisterHubSite \u00b6 Un-registers the current site collection as hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // make a site no longer a hub await sp.site.unRegisterHubSite();","title":"Hubsites"},{"location":"sp/hubsites/#pnpsphubsites","text":"This module helps you with working with hub sites in your tenant.","title":"@pnp/sp/hubsites"},{"location":"sp/hubsites/#ihubsites","text":"Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/hubsites\"; Preset: All import { sp, HubSites, IHubSites } from \"@pnp/sp/presets/all\";","title":"IHubSites"},{"location":"sp/hubsites/#get-a-listing-of-all-hub-sites","text":"import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; // invoke the hub sites object const hubsites: IHubSiteInfo[] = await sp.hubSites(); // you can also use select to only return certain fields: const hubsites2: IHubSiteInfo[] = await sp.hubSites.select(\"ID\", \"Title\", \"RelatedHubSiteIds\")();","title":"Get a Listing of All Hub sites"},{"location":"sp/hubsites/#get-hub-site-by-id","text":"Using the getById method on the hubsites module to get a hub site by site Id (guid). import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; const hubsite: IHubSiteInfo = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\")(); // log hub site title to console console.log(hubsite.Title);","title":"Get Hub site by Id"},{"location":"sp/hubsites/#get-isite-instance","text":"We provide a helper method to load the ISite instance from the HubSite import { sp } from \"@pnp/sp\"; import { ISite } from \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites\"; const site: ISite = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\").getSite(); const siteData = await site(); console.log(siteData.Title);","title":"Get ISite instance"},{"location":"sp/hubsites/#get-hub-site-data-for-a-web","text":"import { sp } from \"@pnp/sp\"; import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; const webData: Partial = await sp.web.hubSiteData(); // you can also force a refresh of the hub site data const webData2: Partial = await sp.web.hubSiteData(true);","title":"Get Hub site data for a web"},{"location":"sp/hubsites/#synchubsitetheme","text":"Allows you to apply theme updates from the parent hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; await sp.web.syncHubSiteTheme();","title":"syncHubSiteTheme"},{"location":"sp/hubsites/#hub-site-site-methods","text":"You manage hub sites at the Site level.","title":"Hub site Site Methods"},{"location":"sp/hubsites/#joinhubsite","text":"Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // join a site to a hub site await sp.site.joinHubSite(\"{parent hub site id}\"); // remove a site from a hub site await sp.site.joinHubSite(\"00000000-0000-0000-0000-000000000000\");","title":"joinHubSite"},{"location":"sp/hubsites/#registerhubsite","text":"Registers the current site collection as hub site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // register current site as a hub site await sp.site.registerHubSite();","title":"registerHubSite"},{"location":"sp/hubsites/#unregisterhubsite","text":"Un-registers the current site collection as hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // make a site no longer a hub await sp.site.unRegisterHubSite();","title":"unRegisterHubSite"},{"location":"sp/items/","text":"@pnp/sp/items \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\"; GET \u00b6 Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions. Basic Get \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // get all the items from a list const items: any[] = await sp.web.lists.getByTitle(\"My List\").items(); console.log(items); // get a specific item by id. const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); console.log(item); // use odata operators for more efficient queries const items2: any[] = await sp.web.lists.getByTitle(\"My List\").items.select(\"Title\", \"Description\").top(5).orderBy(\"Modified\", true)(); console.log(items2); Get Paged Items \u00b6 Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic case to get paged items form a list let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged(); // you can also provide a type for the returned values instead of any let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged<{Title: string}[]>(); // the query also works with select to choose certain fields and top to set the page size let items = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\", \"Description\").top(50).getPaged<{Title: string}[]>(); // the results object will have two properties and one method: // the results property will be an array of the items returned if (items.results.length > 0) { console.log(\"We got results!\"); for (let i = 0; i < items.results.length; i++) { // type checking works here if we specify the return type console.log(items.results[i].Title); } } // the hasNext property is used with the getNext method to handle paging // hasNext will be true so long as there are additional results if (items.hasNext) { // this will carry over the type specified in the original query for the results array items = await items.getNext(); console.log(items.results.length); } getListItemChangesSinceToken \u00b6 The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // Using RowLimit. Enables paging let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({RowLimit: '5'}); // Use QueryOptions to make a XML-style query. // Because it's XML we need to escape special characters // Instead of & we use & in the query let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({QueryOptions: ''}); // Get everything. Using null with ChangeToken gets everything let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({ChangeToken: null}); Get All Items \u00b6 Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic usage const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(); console.log(allItems.length); // set page size const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(4000); console.log(allItems.length); // use select and top. top will set page size and override the any value passed to getAll const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").top(4000).getAll(); console.log(allItems.length); // we can also use filter as a supported odata operation, but this will likely fail on large lists const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").filter(\"Title eq 'Test'\").getAll(); console.log(allItems.length); Retrieving Lookup Fields \u00b6 When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const items = await sp.web.lists.getByTitle(\"LookupList\").items.select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(items); const item = await sp.web.lists.getByTitle(\"LookupList\").items.getById(1).select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(item); Filter using Metadata fields \u00b6 To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; const r = await sp.web.lists.getByTitle(\"TaxonomyList\").getItemsByCAMLQuery({ ViewXml: `Term 2`, }); Retrieving PublishingPageImage \u00b6 The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread . Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { Web } from \"@pnp/sp/webs\"; try { const w = Web(\"https://{publishing site url}\"); const r = await w.lists.getByTitle(\"Pages\").items .select(\"Title\", \"FileRef\", \"FieldValuesAsText/MetaInfo\") .expand(\"FieldValuesAsText\") (); // look through the returned items. for (var i = 0; i < r.length; i++) { // the title field value console.log(r[i].Title); // find the value in the MetaInfo string using regex const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig.exec(r[i].FieldValuesAsText.MetaInfo); if (matches !== null && matches.length > 1) { // this wil be the value of the PublishingPageImage field console.log(matches[1]); } } } catch (e) { console.error(e); } Add Items \u00b6 There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { IItemAddResult } from \"@pnp/sp/items\"; // add an item to the list const iar: IItemAddResult = await sp.web.lists.getByTitle(\"My List\").items.add({ Title: \"Title\", Description: \"Description\" }); console.log(iar); Content Type \u00b6 You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; await sp.web.lists.getById(\"4D5A36EA-6E84-4160-8458-65C436DB765C\").items.add({ Title: \"Test 1\", ContentTypeId: \"0x01030058FD86C279252341AB303852303E4DAF\" }); User Fields \u00b6 There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a \"results\" property and an array. Examples for both are shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; const i = await sp.web.lists.getByTitle(\"PeopleFields\").items.add({ Title: getGUID(), User1Id: 9, // allows a single user User2Id: { results: [16, 45] // allows multiple users } }); console.log(i); If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array. import { sp } from \"@pnp/sp\"; const result = await sp.web.lists.getByTitle(\"UserFieldList\").items.getById(1).validateUpdateListItem([{ FieldName: \"UserField\", FieldValue: JSON.stringify([{ \"Key\": \"i:0#.f|membership|person@tenant.com\" }]), }, { FieldName: \"Title\", FieldValue: \"Test - Updated\", }]); Lookup Fields \u00b6 What is said for User Fields is, in general, relevant to Lookup Fields: Lookup Field types: Single-valued lookup Multiple-valued lookup Id suffix should be appended to the end of lookups EntityPropertyName in payloads Numeric Ids for lookups' items should be passed as values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; await sp.web.lists.getByTitle(\"LookupFields\").items.add({ Title: getGUID(), LookupFieldId: 2, // allows a single lookup value MultiLookupFieldId: { results: [ 1, 56 ] // allows multiple lookup value } }); Add Multiple Items \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidadd\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: \"Batch 6\" }, entityTypeFullName).then(b => { console.log(b); }); list.items.inBatch(batch).add({ Title: \"Batch 7\" }, entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\"); Update \u00b6 The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const i = await list.items.getById(1).update({ Title: \"My New Title\", Description: \"Here is a new description\" }); console.log(i); Getting and updating a collection using filter \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // you are getting back a collection here const items: any[] = await sp.web.lists.getByTitle(\"MyList\").items.top(1).filter(\"Title eq 'A Title'\")(); // see if we got something if (items.length > 0) { const updatedItem = await sp.web.lists.getByTitle(\"MyList\").items.getById(items[0].Id).update({ Title: \"Updated Title\", }); console.log(JSON.stringify(updatedItem)); } Update Multiple Items \u00b6 This approach avoids multiple calls for the same list's entity type name. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidupdate\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); // note requirement of \"*\" eTag param - or use a specific eTag value as needed list.items.getById(1).inBatch(batch).update({ Title: \"Batch 6\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); list.items.getById(2).inBatch(batch).update({ Title: \"Batch 7\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\") Recycle \u00b6 To send an item to the recycle bin use recycle. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const recycleBinIdentifier = await list.items.getById(1).recycle(); Delete \u00b6 Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).delete(); Delete With Params \u00b6 Added in 2.0.9 Deletes the item object with options. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).deleteWithParams({ BypassSharedLock: true, }); The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true Resolving field names \u00b6 It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used. The easiest way to get know EntityPropertyName is to use the following snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import \"@pnp/sp/fields\"; const response = await sp.web.lists .getByTitle('[Lists_Title]') .fields .select('Title, EntityPropertyName') .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) (); console.log(response.map(field => { return { Title: field.Title, EntityPropertyName: field.EntityPropertyName }; })); Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used. getParentInfos \u00b6 Added in 2.0.12 Gets information about an item, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); await item.getParentInfos();","title":"List Items"},{"location":"sp/items/#pnpspitems","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\";","title":"@pnp/sp/items"},{"location":"sp/items/#get","text":"Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.","title":"GET"},{"location":"sp/items/#basic-get","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // get all the items from a list const items: any[] = await sp.web.lists.getByTitle(\"My List\").items(); console.log(items); // get a specific item by id. const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); console.log(item); // use odata operators for more efficient queries const items2: any[] = await sp.web.lists.getByTitle(\"My List\").items.select(\"Title\", \"Description\").top(5).orderBy(\"Modified\", true)(); console.log(items2);","title":"Basic Get"},{"location":"sp/items/#get-paged-items","text":"Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic case to get paged items form a list let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged(); // you can also provide a type for the returned values instead of any let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged<{Title: string}[]>(); // the query also works with select to choose certain fields and top to set the page size let items = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\", \"Description\").top(50).getPaged<{Title: string}[]>(); // the results object will have two properties and one method: // the results property will be an array of the items returned if (items.results.length > 0) { console.log(\"We got results!\"); for (let i = 0; i < items.results.length; i++) { // type checking works here if we specify the return type console.log(items.results[i].Title); } } // the hasNext property is used with the getNext method to handle paging // hasNext will be true so long as there are additional results if (items.hasNext) { // this will carry over the type specified in the original query for the results array items = await items.getNext(); console.log(items.results.length); }","title":"Get Paged Items"},{"location":"sp/items/#getlistitemchangessincetoken","text":"The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // Using RowLimit. Enables paging let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({RowLimit: '5'}); // Use QueryOptions to make a XML-style query. // Because it's XML we need to escape special characters // Instead of & we use & in the query let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({QueryOptions: ''}); // Get everything. Using null with ChangeToken gets everything let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({ChangeToken: null});","title":"getListItemChangesSinceToken"},{"location":"sp/items/#get-all-items","text":"Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic usage const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(); console.log(allItems.length); // set page size const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(4000); console.log(allItems.length); // use select and top. top will set page size and override the any value passed to getAll const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").top(4000).getAll(); console.log(allItems.length); // we can also use filter as a supported odata operation, but this will likely fail on large lists const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").filter(\"Title eq 'Test'\").getAll(); console.log(allItems.length);","title":"Get All Items"},{"location":"sp/items/#retrieving-lookup-fields","text":"When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const items = await sp.web.lists.getByTitle(\"LookupList\").items.select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(items); const item = await sp.web.lists.getByTitle(\"LookupList\").items.getById(1).select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(item);","title":"Retrieving Lookup Fields"},{"location":"sp/items/#filter-using-metadata-fields","text":"To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; const r = await sp.web.lists.getByTitle(\"TaxonomyList\").getItemsByCAMLQuery({ ViewXml: `Term 2`, });","title":"Filter using Metadata fields"},{"location":"sp/items/#retrieving-publishingpageimage","text":"The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread . Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { Web } from \"@pnp/sp/webs\"; try { const w = Web(\"https://{publishing site url}\"); const r = await w.lists.getByTitle(\"Pages\").items .select(\"Title\", \"FileRef\", \"FieldValuesAsText/MetaInfo\") .expand(\"FieldValuesAsText\") (); // look through the returned items. for (var i = 0; i < r.length; i++) { // the title field value console.log(r[i].Title); // find the value in the MetaInfo string using regex const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig.exec(r[i].FieldValuesAsText.MetaInfo); if (matches !== null && matches.length > 1) { // this wil be the value of the PublishingPageImage field console.log(matches[1]); } } } catch (e) { console.error(e); }","title":"Retrieving PublishingPageImage"},{"location":"sp/items/#add-items","text":"There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { IItemAddResult } from \"@pnp/sp/items\"; // add an item to the list const iar: IItemAddResult = await sp.web.lists.getByTitle(\"My List\").items.add({ Title: \"Title\", Description: \"Description\" }); console.log(iar);","title":"Add Items"},{"location":"sp/items/#content-type","text":"You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; await sp.web.lists.getById(\"4D5A36EA-6E84-4160-8458-65C436DB765C\").items.add({ Title: \"Test 1\", ContentTypeId: \"0x01030058FD86C279252341AB303852303E4DAF\" });","title":"Content Type"},{"location":"sp/items/#user-fields","text":"There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a \"results\" property and an array. Examples for both are shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; const i = await sp.web.lists.getByTitle(\"PeopleFields\").items.add({ Title: getGUID(), User1Id: 9, // allows a single user User2Id: { results: [16, 45] // allows multiple users } }); console.log(i); If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array. import { sp } from \"@pnp/sp\"; const result = await sp.web.lists.getByTitle(\"UserFieldList\").items.getById(1).validateUpdateListItem([{ FieldName: \"UserField\", FieldValue: JSON.stringify([{ \"Key\": \"i:0#.f|membership|person@tenant.com\" }]), }, { FieldName: \"Title\", FieldValue: \"Test - Updated\", }]);","title":"User Fields"},{"location":"sp/items/#lookup-fields","text":"What is said for User Fields is, in general, relevant to Lookup Fields: Lookup Field types: Single-valued lookup Multiple-valued lookup Id suffix should be appended to the end of lookups EntityPropertyName in payloads Numeric Ids for lookups' items should be passed as values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; await sp.web.lists.getByTitle(\"LookupFields\").items.add({ Title: getGUID(), LookupFieldId: 2, // allows a single lookup value MultiLookupFieldId: { results: [ 1, 56 ] // allows multiple lookup value } });","title":"Lookup Fields"},{"location":"sp/items/#add-multiple-items","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidadd\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: \"Batch 6\" }, entityTypeFullName).then(b => { console.log(b); }); list.items.inBatch(batch).add({ Title: \"Batch 7\" }, entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\");","title":"Add Multiple Items"},{"location":"sp/items/#update","text":"The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const i = await list.items.getById(1).update({ Title: \"My New Title\", Description: \"Here is a new description\" }); console.log(i);","title":"Update"},{"location":"sp/items/#getting-and-updating-a-collection-using-filter","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // you are getting back a collection here const items: any[] = await sp.web.lists.getByTitle(\"MyList\").items.top(1).filter(\"Title eq 'A Title'\")(); // see if we got something if (items.length > 0) { const updatedItem = await sp.web.lists.getByTitle(\"MyList\").items.getById(items[0].Id).update({ Title: \"Updated Title\", }); console.log(JSON.stringify(updatedItem)); }","title":"Getting and updating a collection using filter"},{"location":"sp/items/#update-multiple-items","text":"This approach avoids multiple calls for the same list's entity type name. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidupdate\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); // note requirement of \"*\" eTag param - or use a specific eTag value as needed list.items.getById(1).inBatch(batch).update({ Title: \"Batch 6\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); list.items.getById(2).inBatch(batch).update({ Title: \"Batch 7\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\")","title":"Update Multiple Items"},{"location":"sp/items/#recycle","text":"To send an item to the recycle bin use recycle. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const recycleBinIdentifier = await list.items.getById(1).recycle();","title":"Recycle"},{"location":"sp/items/#delete","text":"Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).delete();","title":"Delete"},{"location":"sp/items/#delete-with-params","text":"Added in 2.0.9 Deletes the item object with options. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).deleteWithParams({ BypassSharedLock: true, }); The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true","title":"Delete With Params"},{"location":"sp/items/#resolving-field-names","text":"It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used. The easiest way to get know EntityPropertyName is to use the following snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import \"@pnp/sp/fields\"; const response = await sp.web.lists .getByTitle('[Lists_Title]') .fields .select('Title, EntityPropertyName') .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) (); console.log(response.map(field => { return { Title: field.Title, EntityPropertyName: field.EntityPropertyName }; })); Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.","title":"Resolving field names"},{"location":"sp/items/#getparentinfos","text":"Added in 2.0.12 Gets information about an item, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); await item.getParentInfos();","title":"getParentInfos"},{"location":"sp/lists/","text":"@pnp/sp/lists \u00b6 Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet. ILists \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Lists, ILists } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; Preset: All import { sp, Lists, ILists } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Lists, ILists } from \"@pnp/sp/presets/core\"; Get List by Id \u00b6 Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the list by Id const list = sp.web.lists.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // we can use this 'list' variable to execute more queries on the list: const r = await list.select(\"Title\")(); // show the response from the server console.log(r.Title); Get List by Title \u00b6 You can also get a list from the collection by title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the default document library 'Documents' const list = sp.web.lists.getByTitle(\"Documents\"); // we can use this 'list' variable to run more queries on the list: const r = await list.select(\"Id\")(); // log the list Id to console console.log(r.Id); Add List \u00b6 You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide. // create a new list, passing only the title const listAddResult = await sp.web.lists.add(\"My new list\"); // we can work with the list created using the IListAddResult.list property: const r = await listAddResult.list.select(\"Title\")(); // log newly created list title to console console.log(r.Title); }); You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs. // this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings) const listAddResult = await sp.web.lists.add(\"My Doc Library\", \"This is a description of doc lib.\", 101, true, { OnQuickLaunch: true }); // get the Id of the newly added document library const r = await listAddResult.list.select(\"Id\")(); // log id to console console.log(r.Id); Ensure that a List exists (by title) \u00b6 Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings. // ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default): const listEnsureResult = await sp.web.lists.ensure(\"My List\"); // check if the list was created, or if it already existed: if (listEnsureResult.created) { console.log(\"My List was created!\"); } else { console.log(\"My List already existed!\"); } // work on the created/updated list const r = await listEnsureResult.list.select(\"Id\")(); // log the Id console.log(r.Id); If the list already exists, the other settings you provide will be used to update the existing list. // add a new list to the lists collection of the web sp.web.lists.add(\"My List 2\").then(async () => { // then call ensure on the created list with an updated description const listEnsureResult = await sp.web.lists.ensure(\"My List 2\", \"Updated description\"); // get the updated description const r = await listEnsureResult.list.select(\"Description\")(); // log the updated description console.log(r.Description); }); Ensure Site Assets Library exist \u00b6 Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages. // get Site Assets library const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title); Ensure Site Pages Library exist \u00b6 Gets a list that is the default location for wiki pages. // get Site Pages library const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title); IList \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { List, IList } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists\"; Preset: All import { sp, List, IList } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, List, IList } from \"@pnp/sp/presets/core\"; Update a list \u00b6 Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is \"*\") import { IListUpdateResult } from \"@pnp/sp/lists\"; // create a TypedHash object with the properties to update const updateProperties = { Description: \"This list title and description has been updated using PnPjs.\", Title: \"Updated title\", }; // update the list with the properties above list.update(updateProperties).then(async (l: IListUpdateResult) => { // get the updated title and description const r = await l.list.select(\"Title\", \"Description\")(); // log the updated properties to the console console.log(r.Title); console.log(r.Description); }); Get changes on a list \u00b6 From the change log, you can get a collection of changes that have occurred within the list based on the specified query. import { sp, IChangeQuery } from \"@pnp/sp\"; // build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore const changeQuery: IChangeQuery = { Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Rename: true, Restore: true, }; // get list changes const r = await list.getChanges(changeQuery); // log changes to console console.log(r); Get list items using a CAML Query \u00b6 You can get items from SharePoint using a CAML Query. import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml); // log resulting array to console console.log(r); If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment) import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml, \"RoleAssignments\"); // log resulting item array to console console.log(r); Get list items changes using a Token \u00b6 import { IChangeLogItemQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const changeLogItemQuery: IChangeLogItemQuery = { Contains: `Item16`, QueryOptions: ` FALSE False TRUE FALSE My List`, }; // get list items const r = await list.getListItemChangesSinceToken(changeLogItemQuery); // log resulting XML to console console.log(r); Recycle a list \u00b6 Removes the list from the web's list collection and puts it in the recycle bin. await list.recycle(); Render list data \u00b6 import { IRenderListData } from \"@pnp/sp/lists\"; // render list data, top 5 items const r: IRenderListData = await list.renderListData(\"5\"); // log array of items in response console.log(r.Row); Render list data as stream \u00b6 import { IRenderListDataParameters } from \"@pnp/sp/lists\"; // setup parameters object const renderListDataParams: IRenderListDataParameters = { ViewXml: \"5\", }; // render list data as stream const r = await list.renderListDataAsStream(renderListDataParams); // log array of items in response console.log(r.Row); Reserve list item Id for idempotent list item creation \u00b6 const listItemId = await list.reserveListItemId(); // log id to console console.log(listItemId); Get list item entity type name \u00b6 const entityTypeFullName = await list.getListItemEntityTypeFullName(); // log entity type name console.log(entityTypeFullName); Add a list item using path (folder), validation and set field values \u00b6 const list = await sp.webs.lists.getByTitle(\"MyList\").select(\"Title\", \"ParentWebUrl\")(); const formValues: IListItemFormUpdateValue[] = [ { FieldName: \"Title\", FieldValue: title, }, ]; list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`) content-types imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; contentTypes \u00b6 Get all content types for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.contentTypes(); fields imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; fields \u00b6 Get all the fields for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.fields(); Add a field to the site, then add the site field to a list const fld = await sp.site.rootWeb.fields.addText(\"MyField\"); await sp.web.lists.getByTitle(\"MyList\").fields.createFieldAsXml(fld.data.SchemaXml); folders imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; folders \u00b6 Get the root folder of a list. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.rootFolder(); forms imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/forms\"; Selective 2 import \"@pnp/sp/forms/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; forms \u00b6 const r = await list.forms(); items imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/items\"; Selective 2 import \"@pnp/sp/items/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; items \u00b6 Get a collection of list items. const r = await list.items(); views imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/views\"; Selective 2 import \"@pnp/sp/views/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; views \u00b6 Get the default view of the list const list = sp.web.lists.getByTitle(\"Documents\"); const views = await list.views(); const defaultView = await list.defaultView(); Get a list view by Id const view = await list.getView(defaultView.Id).select(\"Title\")(); security imports \u00b6 To work with list security, you can import the list methods as follows: import \"@pnp/sp/security/list\"; For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation. subscriptions imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/subscriptions\"; Selective 2 import \"@pnp/sp/subscriptions/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; subscriptions \u00b6 Get all subscriptions on the list const list = sp.web.lists.getByTitle(\"Documents\"); const subscriptions = await list.subscriptions(); user-custom-actions imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; userCustomActions \u00b6 Get a collection of the list's user custom actions. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.userCustomActions(); getParentInfos \u00b6 Added in 2.0.12 Gets information about an list, including details about the parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const list = sp.web.lists.getByTitle(\"Documents\"); await list.getParentInfos();","title":"Lists"},{"location":"sp/lists/#pnpsplists","text":"Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.","title":"@pnp/sp/lists"},{"location":"sp/lists/#ilists","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Lists, ILists } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; Preset: All import { sp, Lists, ILists } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Lists, ILists } from \"@pnp/sp/presets/core\";","title":"ILists"},{"location":"sp/lists/#get-list-by-id","text":"Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the list by Id const list = sp.web.lists.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // we can use this 'list' variable to execute more queries on the list: const r = await list.select(\"Title\")(); // show the response from the server console.log(r.Title);","title":"Get List by Id"},{"location":"sp/lists/#get-list-by-title","text":"You can also get a list from the collection by title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the default document library 'Documents' const list = sp.web.lists.getByTitle(\"Documents\"); // we can use this 'list' variable to run more queries on the list: const r = await list.select(\"Id\")(); // log the list Id to console console.log(r.Id);","title":"Get List by Title"},{"location":"sp/lists/#add-list","text":"You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide. // create a new list, passing only the title const listAddResult = await sp.web.lists.add(\"My new list\"); // we can work with the list created using the IListAddResult.list property: const r = await listAddResult.list.select(\"Title\")(); // log newly created list title to console console.log(r.Title); }); You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs. // this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings) const listAddResult = await sp.web.lists.add(\"My Doc Library\", \"This is a description of doc lib.\", 101, true, { OnQuickLaunch: true }); // get the Id of the newly added document library const r = await listAddResult.list.select(\"Id\")(); // log id to console console.log(r.Id);","title":"Add List"},{"location":"sp/lists/#ensure-that-a-list-exists-by-title","text":"Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings. // ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default): const listEnsureResult = await sp.web.lists.ensure(\"My List\"); // check if the list was created, or if it already existed: if (listEnsureResult.created) { console.log(\"My List was created!\"); } else { console.log(\"My List already existed!\"); } // work on the created/updated list const r = await listEnsureResult.list.select(\"Id\")(); // log the Id console.log(r.Id); If the list already exists, the other settings you provide will be used to update the existing list. // add a new list to the lists collection of the web sp.web.lists.add(\"My List 2\").then(async () => { // then call ensure on the created list with an updated description const listEnsureResult = await sp.web.lists.ensure(\"My List 2\", \"Updated description\"); // get the updated description const r = await listEnsureResult.list.select(\"Description\")(); // log the updated description console.log(r.Description); });","title":"Ensure that a List exists (by title)"},{"location":"sp/lists/#ensure-site-assets-library-exist","text":"Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages. // get Site Assets library const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title);","title":"Ensure Site Assets Library exist"},{"location":"sp/lists/#ensure-site-pages-library-exist","text":"Gets a list that is the default location for wiki pages. // get Site Pages library const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title);","title":"Ensure Site Pages Library exist"},{"location":"sp/lists/#ilist","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { List, IList } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists\"; Preset: All import { sp, List, IList } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, List, IList } from \"@pnp/sp/presets/core\";","title":"IList"},{"location":"sp/lists/#update-a-list","text":"Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is \"*\") import { IListUpdateResult } from \"@pnp/sp/lists\"; // create a TypedHash object with the properties to update const updateProperties = { Description: \"This list title and description has been updated using PnPjs.\", Title: \"Updated title\", }; // update the list with the properties above list.update(updateProperties).then(async (l: IListUpdateResult) => { // get the updated title and description const r = await l.list.select(\"Title\", \"Description\")(); // log the updated properties to the console console.log(r.Title); console.log(r.Description); });","title":"Update a list"},{"location":"sp/lists/#get-changes-on-a-list","text":"From the change log, you can get a collection of changes that have occurred within the list based on the specified query. import { sp, IChangeQuery } from \"@pnp/sp\"; // build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore const changeQuery: IChangeQuery = { Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Rename: true, Restore: true, }; // get list changes const r = await list.getChanges(changeQuery); // log changes to console console.log(r);","title":"Get changes on a list"},{"location":"sp/lists/#get-list-items-using-a-caml-query","text":"You can get items from SharePoint using a CAML Query. import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml); // log resulting array to console console.log(r); If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment) import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml, \"RoleAssignments\"); // log resulting item array to console console.log(r);","title":"Get list items using a CAML Query"},{"location":"sp/lists/#get-list-items-changes-using-a-token","text":"import { IChangeLogItemQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const changeLogItemQuery: IChangeLogItemQuery = { Contains: `Item16`, QueryOptions: ` FALSE False TRUE FALSE My List`, }; // get list items const r = await list.getListItemChangesSinceToken(changeLogItemQuery); // log resulting XML to console console.log(r);","title":"Get list items changes using a Token"},{"location":"sp/lists/#recycle-a-list","text":"Removes the list from the web's list collection and puts it in the recycle bin. await list.recycle();","title":"Recycle a list"},{"location":"sp/lists/#render-list-data","text":"import { IRenderListData } from \"@pnp/sp/lists\"; // render list data, top 5 items const r: IRenderListData = await list.renderListData(\"5\"); // log array of items in response console.log(r.Row);","title":"Render list data"},{"location":"sp/lists/#render-list-data-as-stream","text":"import { IRenderListDataParameters } from \"@pnp/sp/lists\"; // setup parameters object const renderListDataParams: IRenderListDataParameters = { ViewXml: \"5\", }; // render list data as stream const r = await list.renderListDataAsStream(renderListDataParams); // log array of items in response console.log(r.Row);","title":"Render list data as stream"},{"location":"sp/lists/#reserve-list-item-id-for-idempotent-list-item-creation","text":"const listItemId = await list.reserveListItemId(); // log id to console console.log(listItemId);","title":"Reserve list item Id for idempotent list item creation"},{"location":"sp/lists/#get-list-item-entity-type-name","text":"const entityTypeFullName = await list.getListItemEntityTypeFullName(); // log entity type name console.log(entityTypeFullName);","title":"Get list item entity type name"},{"location":"sp/lists/#add-a-list-item-using-path-folder-validation-and-set-field-values","text":"const list = await sp.webs.lists.getByTitle(\"MyList\").select(\"Title\", \"ParentWebUrl\")(); const formValues: IListItemFormUpdateValue[] = [ { FieldName: \"Title\", FieldValue: title, }, ]; list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)","title":"Add a list item using path (folder), validation and set field values"},{"location":"sp/lists/#content-types-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"content-types imports"},{"location":"sp/lists/#contenttypes","text":"Get all content types for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.contentTypes();","title":"contentTypes"},{"location":"sp/lists/#fields-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"fields imports"},{"location":"sp/lists/#fields","text":"Get all the fields for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.fields(); Add a field to the site, then add the site field to a list const fld = await sp.site.rootWeb.fields.addText(\"MyField\"); await sp.web.lists.getByTitle(\"MyList\").fields.createFieldAsXml(fld.data.SchemaXml);","title":"fields"},{"location":"sp/lists/#folders-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"folders imports"},{"location":"sp/lists/#folders","text":"Get the root folder of a list. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.rootFolder();","title":"folders"},{"location":"sp/lists/#forms-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/forms\"; Selective 2 import \"@pnp/sp/forms/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"forms imports"},{"location":"sp/lists/#forms","text":"const r = await list.forms();","title":"forms"},{"location":"sp/lists/#items-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/items\"; Selective 2 import \"@pnp/sp/items/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"items imports"},{"location":"sp/lists/#items","text":"Get a collection of list items. const r = await list.items();","title":"items"},{"location":"sp/lists/#views-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/views\"; Selective 2 import \"@pnp/sp/views/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"views imports"},{"location":"sp/lists/#views","text":"Get the default view of the list const list = sp.web.lists.getByTitle(\"Documents\"); const views = await list.views(); const defaultView = await list.defaultView(); Get a list view by Id const view = await list.getView(defaultView.Id).select(\"Title\")();","title":"views"},{"location":"sp/lists/#security-imports","text":"To work with list security, you can import the list methods as follows: import \"@pnp/sp/security/list\"; For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.","title":"security imports"},{"location":"sp/lists/#subscriptions-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/subscriptions\"; Selective 2 import \"@pnp/sp/subscriptions/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"subscriptions imports"},{"location":"sp/lists/#subscriptions","text":"Get all subscriptions on the list const list = sp.web.lists.getByTitle(\"Documents\"); const subscriptions = await list.subscriptions();","title":"subscriptions"},{"location":"sp/lists/#user-custom-actions-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"user-custom-actions imports"},{"location":"sp/lists/#usercustomactions","text":"Get a collection of the list's user custom actions. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.userCustomActions();","title":"userCustomActions"},{"location":"sp/lists/#getparentinfos","text":"Added in 2.0.12 Gets information about an list, including details about the parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const list = sp.web.lists.getByTitle(\"Documents\"); await list.getParentInfos();","title":"getParentInfos"},{"location":"sp/navigation/","text":"@pnp/sp - navigation \u00b6 Navigation Service \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; getMenuState \u00b6 The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\\,containingcomma NOTE: the , separator can be escaped using the \\ as escape character as done in the example above. The string above would split like: property1 property2 property3,containingcomma import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; // Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. const state = await sp.navigation.getMenuState(); // Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 const state2 = await sp.navigation.getMenuState(\"1002\", 5); // Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 const state3 = await sp.navigation.getMenuState(null, 5, \"CurrentNavSiteMapProviderNoEncode\"); getMenuNodeKey \u00b6 Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; const key = await sp.navigation.getMenuNodeKey(\"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\"); Web Navigation \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; The navigation object contains two properties \"quicklaunch\" and \"topnavigationbar\". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar. Get navigation \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const top = await sp.web.navigation.topNavigationBar(); const quick = await sp.web.navigation.quicklaunch(); For the following examples we will refer to a variable named \"nav\" that is understood to be one of topNavigationBar or quicklaunch. getById \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node = await nav.getById(3)(); add \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const result = await nav.add(\"Node Title\", \"/sites/dev/pages/mypage.aspx\", true); const nodeDataRaw = result.data; // request the data from the created node const nodeData = result.node(); moveAfter \u00b6 Places a navigation node after another node in the tree import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true); const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true); const node1 = await node1result.node(); const node2 = await node2result.node(); await nav.moveAfter(node1.Id, node2.Id); Delete \u00b6 Deletes a given node import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true); let nodes = await nav(); // check we added a node let index = nodes.findIndex(n => n.Id === node1result.data.Id) // index >= 0 // delete a node await nav.getById(node1result.data.Id).delete(); nodes = await nav(); index = nodes.findIndex(n => n.Id === node1result.data.Id) // index = -1 Update \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; await nav.getById(4).update({ Title: \"A new title\", }); Children \u00b6 The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const childrenData = await nav.getById(1).children(); // add a child await nav.getById(1).children.add(\"Title\", \"Url\", true);","title":"Navigation"},{"location":"sp/navigation/#pnpsp-navigation","text":"","title":"@pnp/sp - navigation"},{"location":"sp/navigation/#navigation-service","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\";","title":"Navigation Service"},{"location":"sp/navigation/#getmenustate","text":"The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\\,containingcomma NOTE: the , separator can be escaped using the \\ as escape character as done in the example above. The string above would split like: property1 property2 property3,containingcomma import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; // Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. const state = await sp.navigation.getMenuState(); // Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 const state2 = await sp.navigation.getMenuState(\"1002\", 5); // Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 const state3 = await sp.navigation.getMenuState(null, 5, \"CurrentNavSiteMapProviderNoEncode\");","title":"getMenuState"},{"location":"sp/navigation/#getmenunodekey","text":"Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; const key = await sp.navigation.getMenuNodeKey(\"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\");","title":"getMenuNodeKey"},{"location":"sp/navigation/#web-navigation","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; The navigation object contains two properties \"quicklaunch\" and \"topnavigationbar\". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.","title":"Web Navigation"},{"location":"sp/navigation/#get-navigation","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const top = await sp.web.navigation.topNavigationBar(); const quick = await sp.web.navigation.quicklaunch(); For the following examples we will refer to a variable named \"nav\" that is understood to be one of topNavigationBar or quicklaunch.","title":"Get navigation"},{"location":"sp/navigation/#getbyid","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node = await nav.getById(3)();","title":"getById"},{"location":"sp/navigation/#add","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const result = await nav.add(\"Node Title\", \"/sites/dev/pages/mypage.aspx\", true); const nodeDataRaw = result.data; // request the data from the created node const nodeData = result.node();","title":"add"},{"location":"sp/navigation/#moveafter","text":"Places a navigation node after another node in the tree import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true); const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true); const node1 = await node1result.node(); const node2 = await node2result.node(); await nav.moveAfter(node1.Id, node2.Id);","title":"moveAfter"},{"location":"sp/navigation/#delete","text":"Deletes a given node import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true); let nodes = await nav(); // check we added a node let index = nodes.findIndex(n => n.Id === node1result.data.Id) // index >= 0 // delete a node await nav.getById(node1result.data.Id).delete(); nodes = await nav(); index = nodes.findIndex(n => n.Id === node1result.data.Id) // index = -1","title":"Delete"},{"location":"sp/navigation/#update","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; await nav.getById(4).update({ Title: \"A new title\", });","title":"Update"},{"location":"sp/navigation/#children","text":"The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const childrenData = await nav.getById(1).children(); // add a child await nav.getById(1).children.add(\"Title\", \"Url\", true);","title":"Children"},{"location":"sp/permissions/","text":"@pnp/sp - permissions \u00b6 A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables. Get Role Assignments \u00b6 This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const roles = await sp.web.roleAssignments(); Logger.writeJSON(roles); First Unique Ancestor Securable Object \u00b6 This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const obj = await sp.web.firstUniqueAncestorSecurableObject(); Logger.writeJSON(obj); User Effective Permissions \u00b6 This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const perms = await sp.web.getUserEffectivePermissions(\"i:0#.f|membership|user@site.com\"); Logger.writeJSON(perms); const perms2 = await sp.web.getCurrentUserEffectivePermissions(); Logger.writeJSON(perms2); User Has Permissions \u00b6 Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.userHasPermissions(\"i:0#.f|membership|user@site.com\", PermissionKind.ApproveItems); console.log(perms); const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems); console.log(perms2); Has Permissions \u00b6 If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.getCurrentUserEffectivePermissions(); if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) { // ... }","title":"Permissions"},{"location":"sp/permissions/#pnpsp-permissions","text":"A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.","title":"@pnp/sp - permissions"},{"location":"sp/permissions/#get-role-assignments","text":"This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const roles = await sp.web.roleAssignments(); Logger.writeJSON(roles);","title":"Get Role Assignments"},{"location":"sp/permissions/#first-unique-ancestor-securable-object","text":"This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const obj = await sp.web.firstUniqueAncestorSecurableObject(); Logger.writeJSON(obj);","title":"First Unique Ancestor Securable Object"},{"location":"sp/permissions/#user-effective-permissions","text":"This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const perms = await sp.web.getUserEffectivePermissions(\"i:0#.f|membership|user@site.com\"); Logger.writeJSON(perms); const perms2 = await sp.web.getCurrentUserEffectivePermissions(); Logger.writeJSON(perms2);","title":"User Effective Permissions"},{"location":"sp/permissions/#user-has-permissions","text":"Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.userHasPermissions(\"i:0#.f|membership|user@site.com\", PermissionKind.ApproveItems); console.log(perms); const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems); console.log(perms2);","title":"User Has Permissions"},{"location":"sp/permissions/#has-permissions","text":"If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.getCurrentUserEffectivePermissions(); if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) { // ... }","title":"Has Permissions"},{"location":"sp/profiles/","text":"@pnp/sp/profiles \u00b6 The profile services allows you to work with the SharePoint User Profile Store. Profiles \u00b6 Profiles is accessed directly from the root sp object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/profiles\"; Get edit profile link for the current user \u00b6 editProfileLink(): Promise const editProfileLink = await sp.profiles.editProfileLink(); console.log(\"My edit profile link =\" + editProfileLink); Is My People List Public \u00b6 Provides a boolean that indicates if the current users \"People I'm Following\" list is public or not isMyPeopleListPublic(): Promise const isPublic = await sp.profiles.isMyPeopleListPublic(); console.log(\"Is my Following list Public =\" + isPubic); Find out if the current user is followed by another user \u00b6 Provides a boolean that indicates if the current users is followed by a specific user. amIFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const isFollowedBy = await sp.profiles.amIFollowedBy(loginName); console.log(\"Is \" + loginName + \" following me? \" + isFollowedBy); Find out if I am following a specific user \u00b6 Provides a boolean that indicates if the current users is followed by a specific user. amIFollowing(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const following = await sp.profiles.amIFollowing(loginName); console.log(\"Am I following \" + loginName + \"? \" + following); Get the tags I follow \u00b6 Gets the tags the current user is following. Accepts max count, default is 20. getFollowedTags(maxCount = 20): Promise const tags = await sp.profiles.getFollowedTags(); console.log(tags); Get followers for a specific user \u00b6 Gets the people who are following the specified user. getFollowersFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); }); Get followers for the current \u00b6 Gets the people who are following the current user. myFollowers(): ISharePointQueryableCollection const folowers = await sp.profiles.myFollowers(); console.log(folowers); Get the properties for the current user \u00b6 Gets user properties for the current user. myProperties(): _SharePointQueryableInstance const profile = await sp.profiles.myProperties(); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName); Gets people specified user is following \u00b6 getPeopleFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const folowers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); }); Gets properties for a specified user \u00b6 getPropertiesFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const profile = await sp.profiles.getPropertiesFor(loginName); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in inconvenient Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName); Gets most popular tags \u00b6 Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first trendingTags(): Promise const tags = await sp.profiles.trendingTags(); tags.Items.forEach((tag) => { console.log(tag); }); Gets specified user profile property for the specified user \u00b6 getUserProfilePropertyFor(loginName: string, propertyName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"AccountName\"; const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName); console.log(property); Hide specific user from list of suggested people \u00b6 Removes the specified user from the user's list of suggested people to follow. hideSuggestion(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.hideSuggestion(loginName); Is one user following another \u00b6 Indicates whether the first user is following the second user. First parameter is the account name of the user who might be following the followee. Second parameter is the account name of the user who might be followed by the follower. isFollowing(follower: string, followee: string): Promise const follower = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followee = \"i:0#.f|membership|testuser2@mytenant.onmicrosoft.com\"; const isFollowing = await sp.profiles.isFollowing(follower, followee); console.log(isFollowing); Set User Profile Picture \u00b6 Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB. setMyProfilePic(profilePicSource: Blob): Promise import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/profiles\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files\"; // get the blob object through a request or from a file input const blob = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.getByName(\"profile.jpg\").getBlob(); await sp.profiles.setMyProfilePic(blob); Sets single value User Profile property \u00b6 accountName The account name of the user propertyName Property name propertyValue Property value setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.setSingleValueProfileProperty(loginName, \"CellPhone\", \"(123) 555-1212\"); Sets a mult-value User Profile property \u00b6 accountName The account name of the user propertyName Property name propertyValues Property values setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"SPS-Skills\"; const propertyValues = [\"SharePoint\", \"Office 365\", \"Architecture\", \"Azure\"]; await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues); const profile = await sp.profiles.getPropertiesFor(loginName); var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(profile.userProperties[propertyName]); Create Personal Site for specified users \u00b6 Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Emails The email addresses of the users to provision sites for createPersonalSiteEnqueueBulk(...emails: string[]): Promise let userEmails: string[] = [\"testuser1@mytenant.onmicrosoft.com\", \"testuser2@mytenant.onmicrosoft.com\"]; await sp.profiles.createPersonalSiteEnqueueBulk(userEmails); Get the user profile of the owner for the current site \u00b6 ownerUserProfile(): Promise const profile = await sp.profiles.ownerUserProfile(); console.log(profile); Get the user profile of the current user \u00b6 userProfile(): Promise const profile = await sp.profiles.userProfile(); console.log(profile); Create personal site for current user \u00b6 createPersonalSite(interactiveRequest = false): Promise await sp.profiles.createPersonalSite(); Make all profile data public or private \u00b6 Set the privacy settings for all social data. shareAllSocialData(share: boolean): Promise await sp.profiles.shareAllSocialData(true); Resolve a user or group \u00b6 Resolves user or group using specified query parameters clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result); Search a user or group \u00b6 Searches for users or groups using specified query parameters clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result);","title":"Profiles"},{"location":"sp/profiles/#pnpspprofiles","text":"The profile services allows you to work with the SharePoint User Profile Store.","title":"@pnp/sp/profiles"},{"location":"sp/profiles/#profiles","text":"Profiles is accessed directly from the root sp object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/profiles\";","title":"Profiles"},{"location":"sp/profiles/#get-edit-profile-link-for-the-current-user","text":"editProfileLink(): Promise const editProfileLink = await sp.profiles.editProfileLink(); console.log(\"My edit profile link =\" + editProfileLink);","title":"Get edit profile link for the current user"},{"location":"sp/profiles/#is-my-people-list-public","text":"Provides a boolean that indicates if the current users \"People I'm Following\" list is public or not isMyPeopleListPublic(): Promise const isPublic = await sp.profiles.isMyPeopleListPublic(); console.log(\"Is my Following list Public =\" + isPubic);","title":"Is My People List Public"},{"location":"sp/profiles/#find-out-if-the-current-user-is-followed-by-another-user","text":"Provides a boolean that indicates if the current users is followed by a specific user. amIFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const isFollowedBy = await sp.profiles.amIFollowedBy(loginName); console.log(\"Is \" + loginName + \" following me? \" + isFollowedBy);","title":"Find out if the current user is followed by another user"},{"location":"sp/profiles/#find-out-if-i-am-following-a-specific-user","text":"Provides a boolean that indicates if the current users is followed by a specific user. amIFollowing(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const following = await sp.profiles.amIFollowing(loginName); console.log(\"Am I following \" + loginName + \"? \" + following);","title":"Find out if I am following a specific user"},{"location":"sp/profiles/#get-the-tags-i-follow","text":"Gets the tags the current user is following. Accepts max count, default is 20. getFollowedTags(maxCount = 20): Promise const tags = await sp.profiles.getFollowedTags(); console.log(tags);","title":"Get the tags I follow"},{"location":"sp/profiles/#get-followers-for-a-specific-user","text":"Gets the people who are following the specified user. getFollowersFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); });","title":"Get followers for a specific user"},{"location":"sp/profiles/#get-followers-for-the-current","text":"Gets the people who are following the current user. myFollowers(): ISharePointQueryableCollection const folowers = await sp.profiles.myFollowers(); console.log(folowers);","title":"Get followers for the current"},{"location":"sp/profiles/#get-the-properties-for-the-current-user","text":"Gets user properties for the current user. myProperties(): _SharePointQueryableInstance const profile = await sp.profiles.myProperties(); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName);","title":"Get the properties for the current user"},{"location":"sp/profiles/#gets-people-specified-user-is-following","text":"getPeopleFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const folowers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); });","title":"Gets people specified user is following"},{"location":"sp/profiles/#gets-properties-for-a-specified-user","text":"getPropertiesFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const profile = await sp.profiles.getPropertiesFor(loginName); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in inconvenient Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName);","title":"Gets properties for a specified user"},{"location":"sp/profiles/#gets-most-popular-tags","text":"Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first trendingTags(): Promise const tags = await sp.profiles.trendingTags(); tags.Items.forEach((tag) => { console.log(tag); });","title":"Gets most popular tags"},{"location":"sp/profiles/#gets-specified-user-profile-property-for-the-specified-user","text":"getUserProfilePropertyFor(loginName: string, propertyName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"AccountName\"; const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName); console.log(property);","title":"Gets specified user profile property for the specified user"},{"location":"sp/profiles/#hide-specific-user-from-list-of-suggested-people","text":"Removes the specified user from the user's list of suggested people to follow. hideSuggestion(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.hideSuggestion(loginName);","title":"Hide specific user from list of suggested people"},{"location":"sp/profiles/#is-one-user-following-another","text":"Indicates whether the first user is following the second user. First parameter is the account name of the user who might be following the followee. Second parameter is the account name of the user who might be followed by the follower. isFollowing(follower: string, followee: string): Promise const follower = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followee = \"i:0#.f|membership|testuser2@mytenant.onmicrosoft.com\"; const isFollowing = await sp.profiles.isFollowing(follower, followee); console.log(isFollowing);","title":"Is one user following another"},{"location":"sp/profiles/#set-user-profile-picture","text":"Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB. setMyProfilePic(profilePicSource: Blob): Promise import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/profiles\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files\"; // get the blob object through a request or from a file input const blob = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.getByName(\"profile.jpg\").getBlob(); await sp.profiles.setMyProfilePic(blob);","title":"Set User Profile Picture"},{"location":"sp/profiles/#sets-single-value-user-profile-property","text":"accountName The account name of the user propertyName Property name propertyValue Property value setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.setSingleValueProfileProperty(loginName, \"CellPhone\", \"(123) 555-1212\");","title":"Sets single value User Profile property"},{"location":"sp/profiles/#sets-a-mult-value-user-profile-property","text":"accountName The account name of the user propertyName Property name propertyValues Property values setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"SPS-Skills\"; const propertyValues = [\"SharePoint\", \"Office 365\", \"Architecture\", \"Azure\"]; await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues); const profile = await sp.profiles.getPropertiesFor(loginName); var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(profile.userProperties[propertyName]);","title":"Sets a mult-value User Profile property"},{"location":"sp/profiles/#create-personal-site-for-specified-users","text":"Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Emails The email addresses of the users to provision sites for createPersonalSiteEnqueueBulk(...emails: string[]): Promise let userEmails: string[] = [\"testuser1@mytenant.onmicrosoft.com\", \"testuser2@mytenant.onmicrosoft.com\"]; await sp.profiles.createPersonalSiteEnqueueBulk(userEmails);","title":"Create Personal Site for specified users"},{"location":"sp/profiles/#get-the-user-profile-of-the-owner-for-the-current-site","text":"ownerUserProfile(): Promise const profile = await sp.profiles.ownerUserProfile(); console.log(profile);","title":"Get the user profile of the owner for the current site"},{"location":"sp/profiles/#get-the-user-profile-of-the-current-user","text":"userProfile(): Promise const profile = await sp.profiles.userProfile(); console.log(profile);","title":"Get the user profile of the current user"},{"location":"sp/profiles/#create-personal-site-for-current-user","text":"createPersonalSite(interactiveRequest = false): Promise await sp.profiles.createPersonalSite();","title":"Create personal site for current user"},{"location":"sp/profiles/#make-all-profile-data-public-or-private","text":"Set the privacy settings for all social data. shareAllSocialData(share: boolean): Promise await sp.profiles.shareAllSocialData(true);","title":"Make all profile data public or private"},{"location":"sp/profiles/#resolve-a-user-or-group","text":"Resolves user or group using specified query parameters clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result);","title":"Resolve a user or group"},{"location":"sp/profiles/#search-a-user-or-group","text":"Searches for users or groups using specified query parameters clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result);","title":"Search a user or group"},{"location":"sp/regional-settings/","text":"@pnp/sp/regional-settings \u00b6 The regional settings module helps with managing dates and times across various timezones. IRegionalSettings \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IRegionalSettings, ITimeZone, ITimeZones, RegionalSettings, TimeZone, TimeZones, } from \"@pnp/sp/regional-settings\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get all the web's regional settings const s = await sp.web.regionalSettings(); // select only some settings to return const s2 = await sp.web.regionalSettings.select(\"DecimalSeparator\", \"ListSeparator\", \"IsUIRightToLeft\")(); Installed Languages \u00b6 You can get a list of the installed languages in the web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; const s = await sp.web.regionalSettings.getInstalledLanguages(); The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions. TimeZones \u00b6 You can also get information about the selected timezone in the web and all of the defined timezones. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get the web's configured timezone const s = await sp.web.regionalSettings.timeZone(); // select just the Description and Id const s2 = await sp.web.regionalSettings.timeZone.select(\"Description\", \"Id\")(); // get all the timezones const s3 = await sp.web.regionalSettings.timeZones(); // get a specific timezone by id // list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx const s4 = await sp.web.regionalSettings.timeZones.getById(23); const s5 = await s.localTimeToUTC(new Date()); // convert a given date from web's local time to UTC time const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date()); // convert a given date from UTC time to web's local time const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date()) const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0)) Title and Description Resources \u00b6 Added in 2.0.4 Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; // // The below methods appears on // - Web // - List // - Field // - ContentType // - User Custom Action // // after you import @pnp/sp/regional-settings // // you can also import just parts of the regional settings: // import \"@pnp/sp/regional-settings/web\"; // import \"@pnp/sp/regional-settings/list\"; // import \"@pnp/sp/regional-settings/content-type\"; // import \"@pnp/sp/regional-settings/field\"; // import \"@pnp/sp/regional-settings/user-custom-actions\"; const title = await sp.web.titleResource(\"en-us\"); const title2 = await sp.web.titleResource(\"de-de\"); const description = await sp.web.descriptionResource(\"en-us\"); const description2 = await sp.web.descriptionResource(\"de-de\"); You can only read the values through the REST API, not set the value.","title":"Regional Settings"},{"location":"sp/regional-settings/#pnpspregional-settings","text":"The regional settings module helps with managing dates and times across various timezones.","title":"@pnp/sp/regional-settings"},{"location":"sp/regional-settings/#iregionalsettings","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IRegionalSettings, ITimeZone, ITimeZones, RegionalSettings, TimeZone, TimeZones, } from \"@pnp/sp/regional-settings\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get all the web's regional settings const s = await sp.web.regionalSettings(); // select only some settings to return const s2 = await sp.web.regionalSettings.select(\"DecimalSeparator\", \"ListSeparator\", \"IsUIRightToLeft\")();","title":"IRegionalSettings"},{"location":"sp/regional-settings/#installed-languages","text":"You can get a list of the installed languages in the web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; const s = await sp.web.regionalSettings.getInstalledLanguages(); The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.","title":"Installed Languages"},{"location":"sp/regional-settings/#timezones","text":"You can also get information about the selected timezone in the web and all of the defined timezones. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get the web's configured timezone const s = await sp.web.regionalSettings.timeZone(); // select just the Description and Id const s2 = await sp.web.regionalSettings.timeZone.select(\"Description\", \"Id\")(); // get all the timezones const s3 = await sp.web.regionalSettings.timeZones(); // get a specific timezone by id // list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx const s4 = await sp.web.regionalSettings.timeZones.getById(23); const s5 = await s.localTimeToUTC(new Date()); // convert a given date from web's local time to UTC time const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date()); // convert a given date from UTC time to web's local time const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date()) const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))","title":"TimeZones"},{"location":"sp/regional-settings/#title-and-description-resources","text":"Added in 2.0.4 Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; // // The below methods appears on // - Web // - List // - Field // - ContentType // - User Custom Action // // after you import @pnp/sp/regional-settings // // you can also import just parts of the regional settings: // import \"@pnp/sp/regional-settings/web\"; // import \"@pnp/sp/regional-settings/list\"; // import \"@pnp/sp/regional-settings/content-type\"; // import \"@pnp/sp/regional-settings/field\"; // import \"@pnp/sp/regional-settings/user-custom-actions\"; const title = await sp.web.titleResource(\"en-us\"); const title2 = await sp.web.titleResource(\"de-de\"); const description = await sp.web.descriptionResource(\"en-us\"); const description2 = await sp.web.descriptionResource(\"de-de\"); You can only read the values through the REST API, not set the value.","title":"Title and Description Resources"},{"location":"sp/related-items/","text":"@pnp/sp/related-items \u00b6 The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection. Setup \u00b6 Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work. import { sp, extractWebUrl } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/related-items/web\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import \"@pnp/sp/files/list\"; import { IList } from \"@pnp/sp/lists\"; import { getRandomString } from \"@pnp/core\"; // setup some lists (or just use existing ones this is just to show the complete process) // we need two lists to use for creating related items, they need to use template 107 (task list) const ler1 = await sp.web.lists.ensure(\"RelatedItemsSourceList\", \"\", 107); const ler2 = await sp.web.lists.ensure(\"RelatedItemsTargetList\", \"\", 107); const sourceList = ler1.list; const targetList = ler2.list; const sourceListName = await sourceList.select(\"Id\")().then(r => r.Id); const targetListName = await targetList.select(\"Id\")().then(r => r.Id); // or whatever you need to get the web url, both our example lists are in the same web. const webUrl = sp.web.toUrl(); // ...individual samples start here addSingleLink \u00b6 const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); addSingleLinkToUrl \u00b6 This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document. // get a file's server relative url in some manner, here we add one const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, \"Content\", true).then(r => r.data); // add an item or get an item from the task list const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl); addSingleLinkFromUrl \u00b6 This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method. deleteSingleLink \u00b6 This method allows you to delete a link previously created. const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add the link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); // delete the link await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); getRelatedItems \u00b6 Gets the related items for an item import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id); // items.length === 2 Related items are defined by the IRelatedItem interface export interface IRelatedItem { ListId: string; ItemId: number; Url: string; Title: string; WebId: string; IconUrl: string; } getPageOneRelatedItems \u00b6 Gets an abbreviated set of related items import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id); // items.length === 2","title":"Related Items"},{"location":"sp/related-items/#pnpsprelated-items","text":"The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.","title":"@pnp/sp/related-items"},{"location":"sp/related-items/#setup","text":"Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work. import { sp, extractWebUrl } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/related-items/web\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import \"@pnp/sp/files/list\"; import { IList } from \"@pnp/sp/lists\"; import { getRandomString } from \"@pnp/core\"; // setup some lists (or just use existing ones this is just to show the complete process) // we need two lists to use for creating related items, they need to use template 107 (task list) const ler1 = await sp.web.lists.ensure(\"RelatedItemsSourceList\", \"\", 107); const ler2 = await sp.web.lists.ensure(\"RelatedItemsTargetList\", \"\", 107); const sourceList = ler1.list; const targetList = ler2.list; const sourceListName = await sourceList.select(\"Id\")().then(r => r.Id); const targetListName = await targetList.select(\"Id\")().then(r => r.Id); // or whatever you need to get the web url, both our example lists are in the same web. const webUrl = sp.web.toUrl(); // ...individual samples start here","title":"Setup"},{"location":"sp/related-items/#addsinglelink","text":"const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);","title":"addSingleLink"},{"location":"sp/related-items/#addsinglelinktourl","text":"This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document. // get a file's server relative url in some manner, here we add one const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, \"Content\", true).then(r => r.data); // add an item or get an item from the task list const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);","title":"addSingleLinkToUrl"},{"location":"sp/related-items/#addsinglelinkfromurl","text":"This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.","title":"addSingleLinkFromUrl"},{"location":"sp/related-items/#deletesinglelink","text":"This method allows you to delete a link previously created. const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add the link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); // delete the link await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);","title":"deleteSingleLink"},{"location":"sp/related-items/#getrelateditems","text":"Gets the related items for an item import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id); // items.length === 2 Related items are defined by the IRelatedItem interface export interface IRelatedItem { ListId: string; ItemId: number; Url: string; Title: string; WebId: string; IconUrl: string; }","title":"getRelatedItems"},{"location":"sp/related-items/#getpageonerelateditems","text":"Gets an abbreviated set of related items import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id); // items.length === 2","title":"getPageOneRelatedItems"},{"location":"sp/search/","text":"@pnp/sp/search \u00b6 Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier. Search \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults } from \"@pnp/sp/search\"; Preset: All import { sp, ISearchQuery, SearchResults } from \"@pnp/sp/presets/all\"; Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // text search using SharePoint default values for other parameters const results: SearchResults = await sp.search(\"test\"); console.log(results.ElapsedTime); console.log(results.RowCount); console.log(results.PrimarySearchResults); // define a search query object matching the ISearchQuery interface const results2: SearchResults = await sp.search({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, }); console.log(results2.ElapsedTime); console.log(results2.RowCount); console.log(results2.PrimarySearchResults); // define a query using a builder const builder = SearchQueryBuilder(\"test\").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites; const results3 = await sp.search(builder); console.log(results3.ElapsedTime); console.log(results3.RowCount); console.log(results3.PrimarySearchResults); Search Result Caching \u00b6 You can use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace \"search\" with \"searchWithCaching\" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; sp.searchWithCaching({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, } as ISearchQuery).then((r: SearchResults) => { console.log(r.ElapsedTime); console.log(r.RowCount); console.log(r.PrimarySearchResults); }); // use a query builder const builder = SearchQueryBuilder(\"test\").rowLimit(3); // supply a search query builder and caching options const results2 = await sp.searchWithCaching(builder, { key: \"mykey\", expiration: dateAdd(new Date(), \"month\", 1) }); console.log(results2.TotalRows); Paging with SearchResults.getPage \u00b6 Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // this will hold our current results let currentResults: SearchResults = null; let page = 1; // triggered on page load or through some other means function onStart() { // construct our query that will be used throughout the paging process, likely from user input const q = SearchQueryBuilder(\"test\").rowLimit(5); const results = await sp.search(q); currentResults = results; // set the current results page = 1; // reset page counter // update UI... } // triggered by an event async function next() { currentResults = await currentResults.getPage(++page); // update UI... } // triggered by an event async function prev() { currentResults = await currentResults.getPage(--page); // update UI... } SearchQueryBuilder \u00b6 The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchQueryBuilder, SearchResults, ISearchQuery } from \"@pnp/sp/search\"; // basic usage let q = SearchQueryBuilder().text(\"test\").rowLimit(4).enablePhonetic; sp.search(q).then(h => { /* ... */ }); // provide a default query text at creation let q2 = SearchQueryBuilder(\"text\").rowLimit(4).enablePhonetic; const results: SearchResults = await sp.search(q2); // provide query text and a template for // shared settings across queries that can // be overwritten by individual builders const appSearchSettings: ISearchQuery = { EnablePhonetic: true, HiddenConstraints: \"reports\" }; let q3 = SearchQueryBuilder(\"test\", appSearchSettings).enableQueryRules; let q4 = SearchQueryBuilder(\"financial data\", appSearchSettings).enableSorting.enableStemming; const results2 = await sp.search(q3); const results3 = sp.search(q4); Search Suggest \u00b6 Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISuggestQuery, ISuggestResult } from \"@pnp/sp/search\"; const results = await sp.searchSuggest(\"test\"); const results2 = await sp.searchSuggest({ querytext: \"test\", count: 5, } as ISuggestQuery); Search Factory \u00b6 You can also configure a search or suggest query against any valid SP url using the factory methods. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { Search, Suggest } from \"@pnp/sp/search\"; // set the url for search const searcher = Search(\"https://mytenant.sharepoint.com/sites/dev\"); // this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder) const results = await searcher(\"test\"); // you can reuse the ISearch instance const results2 = await searcher(\"another query\"); // same process works for Suggest const suggester = Suggest(\"https://mytenant.sharepoint.com/sites/dev\"); const suggestions = await suggester({ querytext: \"test\" });","title":"Search"},{"location":"sp/search/#pnpspsearch","text":"Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.","title":"@pnp/sp/search"},{"location":"sp/search/#search","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults } from \"@pnp/sp/search\"; Preset: All import { sp, ISearchQuery, SearchResults } from \"@pnp/sp/presets/all\"; Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // text search using SharePoint default values for other parameters const results: SearchResults = await sp.search(\"test\"); console.log(results.ElapsedTime); console.log(results.RowCount); console.log(results.PrimarySearchResults); // define a search query object matching the ISearchQuery interface const results2: SearchResults = await sp.search({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, }); console.log(results2.ElapsedTime); console.log(results2.RowCount); console.log(results2.PrimarySearchResults); // define a query using a builder const builder = SearchQueryBuilder(\"test\").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites; const results3 = await sp.search(builder); console.log(results3.ElapsedTime); console.log(results3.RowCount); console.log(results3.PrimarySearchResults);","title":"Search"},{"location":"sp/search/#search-result-caching","text":"You can use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace \"search\" with \"searchWithCaching\" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; sp.searchWithCaching({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, } as ISearchQuery).then((r: SearchResults) => { console.log(r.ElapsedTime); console.log(r.RowCount); console.log(r.PrimarySearchResults); }); // use a query builder const builder = SearchQueryBuilder(\"test\").rowLimit(3); // supply a search query builder and caching options const results2 = await sp.searchWithCaching(builder, { key: \"mykey\", expiration: dateAdd(new Date(), \"month\", 1) }); console.log(results2.TotalRows);","title":"Search Result Caching"},{"location":"sp/search/#paging-with-searchresultsgetpage","text":"Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // this will hold our current results let currentResults: SearchResults = null; let page = 1; // triggered on page load or through some other means function onStart() { // construct our query that will be used throughout the paging process, likely from user input const q = SearchQueryBuilder(\"test\").rowLimit(5); const results = await sp.search(q); currentResults = results; // set the current results page = 1; // reset page counter // update UI... } // triggered by an event async function next() { currentResults = await currentResults.getPage(++page); // update UI... } // triggered by an event async function prev() { currentResults = await currentResults.getPage(--page); // update UI... }","title":"Paging with SearchResults.getPage"},{"location":"sp/search/#searchquerybuilder","text":"The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchQueryBuilder, SearchResults, ISearchQuery } from \"@pnp/sp/search\"; // basic usage let q = SearchQueryBuilder().text(\"test\").rowLimit(4).enablePhonetic; sp.search(q).then(h => { /* ... */ }); // provide a default query text at creation let q2 = SearchQueryBuilder(\"text\").rowLimit(4).enablePhonetic; const results: SearchResults = await sp.search(q2); // provide query text and a template for // shared settings across queries that can // be overwritten by individual builders const appSearchSettings: ISearchQuery = { EnablePhonetic: true, HiddenConstraints: \"reports\" }; let q3 = SearchQueryBuilder(\"test\", appSearchSettings).enableQueryRules; let q4 = SearchQueryBuilder(\"financial data\", appSearchSettings).enableSorting.enableStemming; const results2 = await sp.search(q3); const results3 = sp.search(q4);","title":"SearchQueryBuilder"},{"location":"sp/search/#search-suggest","text":"Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISuggestQuery, ISuggestResult } from \"@pnp/sp/search\"; const results = await sp.searchSuggest(\"test\"); const results2 = await sp.searchSuggest({ querytext: \"test\", count: 5, } as ISuggestQuery);","title":"Search Suggest"},{"location":"sp/search/#search-factory","text":"You can also configure a search or suggest query against any valid SP url using the factory methods. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { Search, Suggest } from \"@pnp/sp/search\"; // set the url for search const searcher = Search(\"https://mytenant.sharepoint.com/sites/dev\"); // this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder) const results = await searcher(\"test\"); // you can reuse the ISearch instance const results2 = await searcher(\"another query\"); // same process works for Suggest const suggester = Suggest(\"https://mytenant.sharepoint.com/sites/dev\"); const suggestions = await suggester({ querytext: \"test\" });","title":"Search Factory"},{"location":"sp/security/","text":"@pnp/sp/security \u00b6 There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below. Site permissions are managed on the root web of the site collection. A Note on Selective Imports for Security \u00b6 Because the method are shared you can opt to import only the methods for one of the instances. import \"@pnp/sp/security/web\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/security/item\"; Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module: import \"@pnp/sp/security\"; Securable Methods \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // role assignments (see section below) await list.roleAssignments(); // data will represent one of the possible parents Site, Web, or List const data = await list.firstUniqueAncestorSecurableObject(); // getUserEffectivePermissions const users = await sp.web.siteUsers.top(1).select(\"LoginName\")(); const perms = await list.getUserEffectivePermissions(users[0].LoginName); // getCurrentUserEffectivePermissions const perms2 = list.getCurrentUserEffectivePermissions(); // userHasPermissions const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems) // currentUserHasPermissions const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems) // breakRoleInheritance await list.breakRoleInheritance(); // copy existing permissions await list.breakRoleInheritance(true); // copy existing permissions and reset all child securables to the new permissions await list.breakRoleInheritance(true, true); // resetRoleInheritance await list.resetRoleInheritance(); Web Specific methods \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // role definitions (see section below) const defs = await sp.web.roleDefinitions(); Role Assignments \u00b6 Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/web\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // list role assignments const assignments = await list.roleAssignments(); // add a role assignment const defs = await sp.web.roleDefinitions(); const user = await sp.web.currentUser(); const r = await list.roleAssignments.add(user.Id, defs[0].Id); // remove a role assignment const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); const r = await list.roleAssignments.remove(ra.Id); // read role assignment info const info = await list.roleAssignments.getById(ra.Id)(); // get the groups const info2 = await list.roleAssignments.getById(ra.Id).groups(); // get the bindings const info3 = await list.roleAssignments.getById(ra.Id).bindings(); // delete a role assignment (same as remove) const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); // delete it await list.roleAssignments.getById(ra.Id).delete(); Role Definitions \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // read role definitions const defs = await sp.web.roleDefinitions(); // get by id const def = await sp.web.roleDefinitions.getById(5)(); const def = await sp.web.roleDefinitions.getById(5).select(\"Name\", \"Order\")(); // get by name const def = await sp.web.roleDefinitions.getByName(\"Full Control\")(); const def = await sp.web.roleDefinitions.getByName(\"Full Control\").select(\"Name\", \"Order\")(); // get by type const def = await sp.web.roleDefinitions.getByName(5)(); const def = await sp.web.roleDefinitions.getByName(5).select(\"Name\", \"Order\")(); // add // name The new role definition's name // description The new role definition's description // order The order in which the role definition appears // basePermissions The permissions mask for this role definition const rdar = await sp.web.roleDefinitions.add(\"title\", \"description\", 99, { High: 1, Low: 2 }); // the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example // delete await sp.web.roleDefinitions.getById(5).delete(); // update const res = sp.web.roleDefinitions.getById(5).update({ Name: \"New Name\" });","title":"Security"},{"location":"sp/security/#pnpspsecurity","text":"There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below. Site permissions are managed on the root web of the site collection.","title":"@pnp/sp/security"},{"location":"sp/security/#a-note-on-selective-imports-for-security","text":"Because the method are shared you can opt to import only the methods for one of the instances. import \"@pnp/sp/security/web\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/security/item\"; Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module: import \"@pnp/sp/security\";","title":"A Note on Selective Imports for Security"},{"location":"sp/security/#securable-methods","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // role assignments (see section below) await list.roleAssignments(); // data will represent one of the possible parents Site, Web, or List const data = await list.firstUniqueAncestorSecurableObject(); // getUserEffectivePermissions const users = await sp.web.siteUsers.top(1).select(\"LoginName\")(); const perms = await list.getUserEffectivePermissions(users[0].LoginName); // getCurrentUserEffectivePermissions const perms2 = list.getCurrentUserEffectivePermissions(); // userHasPermissions const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems) // currentUserHasPermissions const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems) // breakRoleInheritance await list.breakRoleInheritance(); // copy existing permissions await list.breakRoleInheritance(true); // copy existing permissions and reset all child securables to the new permissions await list.breakRoleInheritance(true, true); // resetRoleInheritance await list.resetRoleInheritance();","title":"Securable Methods"},{"location":"sp/security/#web-specific-methods","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // role definitions (see section below) const defs = await sp.web.roleDefinitions();","title":"Web Specific methods"},{"location":"sp/security/#role-assignments","text":"Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/web\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // list role assignments const assignments = await list.roleAssignments(); // add a role assignment const defs = await sp.web.roleDefinitions(); const user = await sp.web.currentUser(); const r = await list.roleAssignments.add(user.Id, defs[0].Id); // remove a role assignment const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); const r = await list.roleAssignments.remove(ra.Id); // read role assignment info const info = await list.roleAssignments.getById(ra.Id)(); // get the groups const info2 = await list.roleAssignments.getById(ra.Id).groups(); // get the bindings const info3 = await list.roleAssignments.getById(ra.Id).bindings(); // delete a role assignment (same as remove) const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); // delete it await list.roleAssignments.getById(ra.Id).delete();","title":"Role Assignments"},{"location":"sp/security/#role-definitions","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // read role definitions const defs = await sp.web.roleDefinitions(); // get by id const def = await sp.web.roleDefinitions.getById(5)(); const def = await sp.web.roleDefinitions.getById(5).select(\"Name\", \"Order\")(); // get by name const def = await sp.web.roleDefinitions.getByName(\"Full Control\")(); const def = await sp.web.roleDefinitions.getByName(\"Full Control\").select(\"Name\", \"Order\")(); // get by type const def = await sp.web.roleDefinitions.getByName(5)(); const def = await sp.web.roleDefinitions.getByName(5).select(\"Name\", \"Order\")(); // add // name The new role definition's name // description The new role definition's description // order The order in which the role definition appears // basePermissions The permissions mask for this role definition const rdar = await sp.web.roleDefinitions.add(\"title\", \"description\", 99, { High: 1, Low: 2 }); // the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example // delete await sp.web.roleDefinitions.getById(5).delete(); // update const res = sp.web.roleDefinitions.getById(5).update({ Name: \"New Name\" });","title":"Role Definitions"},{"location":"sp/sharing/","text":"@pnp/sp/sharing \u00b6 Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online. One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue. Imports \u00b6 In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects. Import All \u00b6 To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module: import \"@pnp/sp/sharing\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName); Selective Import \u00b6 Import only the web's sharing methods into the library import \"@pnp/sp/sharing/web\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName); getShareLink \u00b6 Applies to: Item, Folder, File Creates a sharing link for the given resource with an optional expiration. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { SharingLinkKind, IShareLinkResponse } from \"@pnp/sp/sharing\"; import { dateAdd } from \"@pnp/core\"; const result = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView); console.log(JSON.stringify(result, null, 2)); const result2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), \"day\", 5)); console.log(JSON.stringify(result2, null, 2)); shareWith \u00b6 Applies to: Item, Folder, File, Web Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames . The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/files/web\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; const result = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\"); console.log(JSON.stringify(result, null, 2)); // Share and allow editing const result2 = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit); console.log(JSON.stringify(result2, null, 2)); // share folder const result3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children) await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit, true, true); // Share a file await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share a file with edit permissions await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit); shareObject & shareObjectRaw \u00b6 Applies to: Web Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; // Share an object in this web const result = await sp.web.shareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", \"i:0#.f|membership|user@site.com\", SharingRole.View); // Share an object with all settings available await sp.web.shareObjectRaw({ url: \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", peoplePickerInput: [{ Key: \"i:0#.f|membership|user@site.com\" }], roleValue: \"role: 1973741327\", groupId: 0, propagateAcl: false, sendEmail: true, includeAnonymousLinkInEmail: false, emailSubject: \"subject\", emailBody: \"body\", useSimplifiedRoles: true, }); unshareObject \u00b6 Applies to: Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result = await sp.web.unshareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\"); checkSharingPermissions \u00b6 Applies to: Item, Folder, File Checks Permissions on the list of Users and returns back role the users have on the Item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing/folders\"; import \"@pnp/sp/folders/web\"; import { SharingEntityPermission } from \"@pnp/sp/sharing\"; // check the sharing permissions for a folder const perms = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").checkSharingPermissions([{ alias: \"i:0#.f|membership|user@site.com\" }]); getSharingInformation \u00b6 Applies to: Item, Folder, File Get Sharing Information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingInformation } from \"@pnp/sp/sharing\"; // Get the sharing information for a folder const info = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation(); getObjectSharingSettings \u00b6 Applies to: Item, Folder, File Gets the sharing settings import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { IObjectSharingSettings } from \"@pnp/sp/sharing\"; // Gets the sharing object settings const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getObjectSharingSettings(); unshare \u00b6 Applies to: Item, Folder, File Unshares a given resource import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshare(); deleteSharingLinkByKind \u00b6 Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult, SharingLinkKind } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit); unshareLink \u00b6 Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { SharingLinkKind } from \"@pnp/sp/sharing\"; await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit); // specify the sharing link id if available await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit, \"12345\");","title":"Sharing"},{"location":"sp/sharing/#pnpspsharing","text":"Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online. One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.","title":"@pnp/sp/sharing"},{"location":"sp/sharing/#imports","text":"In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.","title":"Imports"},{"location":"sp/sharing/#import-all","text":"To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module: import \"@pnp/sp/sharing\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName);","title":"Import All"},{"location":"sp/sharing/#selective-import","text":"Import only the web's sharing methods into the library import \"@pnp/sp/sharing/web\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName);","title":"Selective Import"},{"location":"sp/sharing/#getsharelink","text":"Applies to: Item, Folder, File Creates a sharing link for the given resource with an optional expiration. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { SharingLinkKind, IShareLinkResponse } from \"@pnp/sp/sharing\"; import { dateAdd } from \"@pnp/core\"; const result = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView); console.log(JSON.stringify(result, null, 2)); const result2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), \"day\", 5)); console.log(JSON.stringify(result2, null, 2));","title":"getShareLink"},{"location":"sp/sharing/#sharewith","text":"Applies to: Item, Folder, File, Web Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames . The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/files/web\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; const result = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\"); console.log(JSON.stringify(result, null, 2)); // Share and allow editing const result2 = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit); console.log(JSON.stringify(result2, null, 2)); // share folder const result3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children) await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit, true, true); // Share a file await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share a file with edit permissions await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit);","title":"shareWith"},{"location":"sp/sharing/#shareobject-shareobjectraw","text":"Applies to: Web Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; // Share an object in this web const result = await sp.web.shareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", \"i:0#.f|membership|user@site.com\", SharingRole.View); // Share an object with all settings available await sp.web.shareObjectRaw({ url: \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", peoplePickerInput: [{ Key: \"i:0#.f|membership|user@site.com\" }], roleValue: \"role: 1973741327\", groupId: 0, propagateAcl: false, sendEmail: true, includeAnonymousLinkInEmail: false, emailSubject: \"subject\", emailBody: \"body\", useSimplifiedRoles: true, });","title":"shareObject & shareObjectRaw"},{"location":"sp/sharing/#unshareobject","text":"Applies to: Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result = await sp.web.unshareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\");","title":"unshareObject"},{"location":"sp/sharing/#checksharingpermissions","text":"Applies to: Item, Folder, File Checks Permissions on the list of Users and returns back role the users have on the Item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing/folders\"; import \"@pnp/sp/folders/web\"; import { SharingEntityPermission } from \"@pnp/sp/sharing\"; // check the sharing permissions for a folder const perms = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").checkSharingPermissions([{ alias: \"i:0#.f|membership|user@site.com\" }]);","title":"checkSharingPermissions"},{"location":"sp/sharing/#getsharinginformation","text":"Applies to: Item, Folder, File Get Sharing Information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingInformation } from \"@pnp/sp/sharing\"; // Get the sharing information for a folder const info = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation();","title":"getSharingInformation"},{"location":"sp/sharing/#getobjectsharingsettings","text":"Applies to: Item, Folder, File Gets the sharing settings import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { IObjectSharingSettings } from \"@pnp/sp/sharing\"; // Gets the sharing object settings const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getObjectSharingSettings();","title":"getObjectSharingSettings"},{"location":"sp/sharing/#unshare","text":"Applies to: Item, Folder, File Unshares a given resource import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshare();","title":"unshare"},{"location":"sp/sharing/#deletesharinglinkbykind","text":"Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult, SharingLinkKind } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);","title":"deleteSharingLinkByKind"},{"location":"sp/sharing/#unsharelink","text":"Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { SharingLinkKind } from \"@pnp/sp/sharing\"; await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit); // specify the sharing link id if available await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit, \"12345\");","title":"unshareLink"},{"location":"sp/site-designs/","text":"@pnp/sp/site-designs \u00b6 You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information. Site Designs \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Create a new site design \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // WebTemplate: 64 Team site template, 68 Communication site template const siteDesign = await sp.siteDesigns.createSiteDesign({ SiteScriptIds: [\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"], Title: \"SiteDesign001\", WebTemplate: \"64\", }); console.log(siteDesign.Title); Applying a site design to a site \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Limited to 30 actions in a site script, but runs synchronously await sp.siteDesigns.applySiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\",\"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\"); // Better use the following method for 300 actions in a site script const task = await sp.web.addSiteDesignTask(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); Retrieval \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Retrieving all site designs const allSiteDesigns = await sp.siteDesigns.getSiteDesigns(); console.log(`Total site designs: ${allSiteDesigns.length}`); // Retrieving a single site design by Id const siteDesign = await sp.siteDesigns.getSiteDesignMetadata(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(siteDesign.Title); Update and delete \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Update const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", Title: \"SiteDesignUpdatedTitle001\" }); // Delete await sp.siteDesigns.deleteSiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); Setting Rights/Permissions \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Get const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(rights.length > 0 ? rights[0].PrincipalName : \"\"); // Grant await sp.siteDesigns.grantSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Revoke await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Reset all view rights const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", rights.map(u => u.PrincipalName)); Get a history of site designs that have run on a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; const runs = await sp.web.getSiteDesignRuns(); const runs2 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\"); // Get runs specific to a site design const runs3 = await sp.web.getSiteDesignRuns(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); const runs4 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\", \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); // For more information about the site script actions const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID); const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus(\"https://TENANT.sharepoint.com/sites/mysite\", runs[0].ID);","title":"Site Designs"},{"location":"sp/site-designs/#pnpspsite-designs","text":"You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information.","title":"@pnp/sp/site-designs"},{"location":"sp/site-designs/#site-designs","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"Site Designs"},{"location":"sp/site-designs/#create-a-new-site-design","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // WebTemplate: 64 Team site template, 68 Communication site template const siteDesign = await sp.siteDesigns.createSiteDesign({ SiteScriptIds: [\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"], Title: \"SiteDesign001\", WebTemplate: \"64\", }); console.log(siteDesign.Title);","title":"Create a new site design"},{"location":"sp/site-designs/#applying-a-site-design-to-a-site","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Limited to 30 actions in a site script, but runs synchronously await sp.siteDesigns.applySiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\",\"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\"); // Better use the following method for 300 actions in a site script const task = await sp.web.addSiteDesignTask(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");","title":"Applying a site design to a site"},{"location":"sp/site-designs/#retrieval","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Retrieving all site designs const allSiteDesigns = await sp.siteDesigns.getSiteDesigns(); console.log(`Total site designs: ${allSiteDesigns.length}`); // Retrieving a single site design by Id const siteDesign = await sp.siteDesigns.getSiteDesignMetadata(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(siteDesign.Title);","title":"Retrieval"},{"location":"sp/site-designs/#update-and-delete","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Update const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", Title: \"SiteDesignUpdatedTitle001\" }); // Delete await sp.siteDesigns.deleteSiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");","title":"Update and delete"},{"location":"sp/site-designs/#setting-rightspermissions","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Get const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(rights.length > 0 ? rights[0].PrincipalName : \"\"); // Grant await sp.siteDesigns.grantSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Revoke await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Reset all view rights const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", rights.map(u => u.PrincipalName));","title":"Setting Rights/Permissions"},{"location":"sp/site-designs/#get-a-history-of-site-designs-that-have-run-on-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; const runs = await sp.web.getSiteDesignRuns(); const runs2 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\"); // Get runs specific to a site design const runs3 = await sp.web.getSiteDesignRuns(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); const runs4 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\", \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); // For more information about the site script actions const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID); const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus(\"https://TENANT.sharepoint.com/sites/mysite\", runs[0].ID);","title":"Get a history of site designs that have run on a web"},{"location":"sp/site-groups/","text":"@pnp/sp/site-groups \u00b6 The site groups module provides methods to manage groups for a sharepoint site. ISiteGroups \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups } from \"@pnp/sp/presets/all\"; Get all site groups \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // gets all site groups of the web const groups = await sp.web.siteGroups(); Get the associated groups of a web \u00b6 You can get the associated Owner, Member and Visitor groups of a web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Gets the associated visitors group of a web const visitorGroup = await sp.web.associatedVisitorGroup(); // Gets the associated members group of a web const memberGroup = await sp.web.associatedMemberGroup(); // Gets the associated owners group of a web const ownerGroup = await sp.web.associatedOwnerGroup(); Create the default associated groups for a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Breaks permission inheritance and creates the default associated groups for the web // Login name of the owner const owner1 = \"owner@example.onmicrosoft.com\"; // Specify true, the permissions should be copied from the current parent scope, else false const copyRoleAssignments = false; // Specify true to make all child securable objects inherit role assignments from the current object const clearSubScopes = true; await sp.web.createDefaultAssociatedGroups(\"PnP Site\", owner1, copyRoleAssignments, clearSubScopes); Create a new site group \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Creates a new site group with the specified title await sp.web.siteGroups.add({\"Title\":\"new group name\"}); ISiteGroup \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups, SiteGroup } from \"@pnp/sp/presets/all\"; Getting and updating the groups of a sharepoint web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get the group using a group id const groupID = 33; let grp = await sp.web.siteGroups.getById(groupID)(); // get the group using the group's name const groupName = \"ClassicTeam Visitors\"; grp = await sp.web.siteGroups.getByName(groupName)(); // update a group await sp.web.siteGroups.getById(groupID).update({\"Title\": \"New Group Title\"}); // delete a group from the site using group id await sp.web.siteGroups.removeById(groupID); // delete a group from the site using group name await sp.web.siteGroups.removeByLoginName(groupName); Getting all users of a group \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get all users of group const groupID = 7; const users = await sp.web.siteGroups.getById(groupID).users(); Updating the owner of a site group \u00b6 Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // Update the owner with a user id await sp.web.siteGroups.getById(7).setUserAsOwner(4);","title":"Site Groups"},{"location":"sp/site-groups/#pnpspsite-groups","text":"The site groups module provides methods to manage groups for a sharepoint site.","title":"@pnp/sp/site-groups"},{"location":"sp/site-groups/#isitegroups","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups } from \"@pnp/sp/presets/all\";","title":"ISiteGroups"},{"location":"sp/site-groups/#get-all-site-groups","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // gets all site groups of the web const groups = await sp.web.siteGroups();","title":"Get all site groups"},{"location":"sp/site-groups/#get-the-associated-groups-of-a-web","text":"You can get the associated Owner, Member and Visitor groups of a web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Gets the associated visitors group of a web const visitorGroup = await sp.web.associatedVisitorGroup(); // Gets the associated members group of a web const memberGroup = await sp.web.associatedMemberGroup(); // Gets the associated owners group of a web const ownerGroup = await sp.web.associatedOwnerGroup();","title":"Get the associated groups of a web"},{"location":"sp/site-groups/#create-the-default-associated-groups-for-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Breaks permission inheritance and creates the default associated groups for the web // Login name of the owner const owner1 = \"owner@example.onmicrosoft.com\"; // Specify true, the permissions should be copied from the current parent scope, else false const copyRoleAssignments = false; // Specify true to make all child securable objects inherit role assignments from the current object const clearSubScopes = true; await sp.web.createDefaultAssociatedGroups(\"PnP Site\", owner1, copyRoleAssignments, clearSubScopes);","title":"Create the default associated groups for a web"},{"location":"sp/site-groups/#create-a-new-site-group","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Creates a new site group with the specified title await sp.web.siteGroups.add({\"Title\":\"new group name\"});","title":"Create a new site group"},{"location":"sp/site-groups/#isitegroup","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups, SiteGroup } from \"@pnp/sp/presets/all\";","title":"ISiteGroup"},{"location":"sp/site-groups/#getting-and-updating-the-groups-of-a-sharepoint-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get the group using a group id const groupID = 33; let grp = await sp.web.siteGroups.getById(groupID)(); // get the group using the group's name const groupName = \"ClassicTeam Visitors\"; grp = await sp.web.siteGroups.getByName(groupName)(); // update a group await sp.web.siteGroups.getById(groupID).update({\"Title\": \"New Group Title\"}); // delete a group from the site using group id await sp.web.siteGroups.removeById(groupID); // delete a group from the site using group name await sp.web.siteGroups.removeByLoginName(groupName);","title":"Getting and updating the groups of a sharepoint web"},{"location":"sp/site-groups/#getting-all-users-of-a-group","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get all users of group const groupID = 7; const users = await sp.web.siteGroups.getById(groupID).users();","title":"Getting all users of a group"},{"location":"sp/site-groups/#updating-the-owner-of-a-site-group","text":"Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // Update the owner with a user id await sp.web.siteGroups.getById(7).setUserAsOwner(4);","title":"Updating the owner of a site group"},{"location":"sp/site-scripts/","text":"@pnp/sp/site-scripts \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Create a new site script \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const sitescriptContent = { \"$schema\": \"schema.json\", \"actions\": [ { \"themeName\": \"Theme Name 123\", \"verb\": \"applyTheme\", }, ], \"bindata\": {}, \"version\": 1, }; const siteScript = await sp.siteScripts.createSiteScript(\"Title\", \"description\", sitescriptContent); console.log(siteScript.Title); Retrieval \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Retrieving all site scripts const allSiteScripts = await sp.siteScripts.getSiteScripts(); console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : \"\"); // Retrieving a single site script by Id const siteScript = await sp.siteScripts.getSiteScriptMetadata(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"); console.log(siteScript.Title); Update and delete \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Update const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\", Title: \"New Title\" }); console.log(updatedSiteScript.Title); // Delete await sp.siteScripts.deleteSiteScript(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"); Get site script from a list \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Using the absolute URL of the list const ss = await sp.siteScripts.getSiteScriptFromList(\"https://TENANT.sharepoint.com/Lists/mylist\"); // Using the PnPjs web object to fetch the site script from a specific list const ss2 = await sp.web.lists.getByTitle(\"mylist\").getSiteScript(); Get site script from a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const extractInfo = { IncludeBranding: true, IncludeLinksToExportedItems: true, IncludeRegionalSettings: true, IncludeSiteExternalSharingCapability: true, IncludeTheme: true, IncludedLists: [\"Lists/MyList\"] }; const ss = await sp.siteScripts.getSiteScriptFromWeb(\"https://TENANT.sharepoint.com/sites/mysite\", extractInfo); // Using the PnPjs web object to fetch the site script from a specific web const ss2 = await sp.web.getSiteScript(extractInfo); Execute Site Script Action \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const ss = await sp.siteScripts.executeSiteScriptAction(siteScript); Execute site script for a specific web \u00b6 import { sp } from \"@pnp/sp\"; import { SiteScripts } \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const scriptService = SiteScripts(\"https://absolute/url/to/web\"); const ss = await scriptService.executeSiteScriptAction(siteScript);","title":"Site Scripts"},{"location":"sp/site-scripts/#pnpspsite-scripts","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/site-scripts"},{"location":"sp/site-scripts/#create-a-new-site-script","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const sitescriptContent = { \"$schema\": \"schema.json\", \"actions\": [ { \"themeName\": \"Theme Name 123\", \"verb\": \"applyTheme\", }, ], \"bindata\": {}, \"version\": 1, }; const siteScript = await sp.siteScripts.createSiteScript(\"Title\", \"description\", sitescriptContent); console.log(siteScript.Title);","title":"Create a new site script"},{"location":"sp/site-scripts/#retrieval","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Retrieving all site scripts const allSiteScripts = await sp.siteScripts.getSiteScripts(); console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : \"\"); // Retrieving a single site script by Id const siteScript = await sp.siteScripts.getSiteScriptMetadata(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"); console.log(siteScript.Title);","title":"Retrieval"},{"location":"sp/site-scripts/#update-and-delete","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Update const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\", Title: \"New Title\" }); console.log(updatedSiteScript.Title); // Delete await sp.siteScripts.deleteSiteScript(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\");","title":"Update and delete"},{"location":"sp/site-scripts/#get-site-script-from-a-list","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Using the absolute URL of the list const ss = await sp.siteScripts.getSiteScriptFromList(\"https://TENANT.sharepoint.com/Lists/mylist\"); // Using the PnPjs web object to fetch the site script from a specific list const ss2 = await sp.web.lists.getByTitle(\"mylist\").getSiteScript();","title":"Get site script from a list"},{"location":"sp/site-scripts/#get-site-script-from-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const extractInfo = { IncludeBranding: true, IncludeLinksToExportedItems: true, IncludeRegionalSettings: true, IncludeSiteExternalSharingCapability: true, IncludeTheme: true, IncludedLists: [\"Lists/MyList\"] }; const ss = await sp.siteScripts.getSiteScriptFromWeb(\"https://TENANT.sharepoint.com/sites/mysite\", extractInfo); // Using the PnPjs web object to fetch the site script from a specific web const ss2 = await sp.web.getSiteScript(extractInfo);","title":"Get site script from a web"},{"location":"sp/site-scripts/#execute-site-script-action","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const ss = await sp.siteScripts.executeSiteScriptAction(siteScript);","title":"Execute Site Script Action"},{"location":"sp/site-scripts/#execute-site-script-for-a-specific-web","text":"import { sp } from \"@pnp/sp\"; import { SiteScripts } \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const scriptService = SiteScripts(\"https://absolute/url/to/web\"); const ss = await scriptService.executeSiteScriptAction(siteScript);","title":"Execute site script for a specific web"},{"location":"sp/site-users/","text":"@pnp/sp/site-users \u00b6 The site users module provides methods to manage users for a sharepoint site. ISiteUsers \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers } from \"@pnp/sp/presets/all\"; Get all site user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const users = await sp.web.siteUsers(); Get Current user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let user = await sp.web.currentUser(); Get user by id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const id = 6; user = await sp.web.getUserById(id); Ensure user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const username = \"usernames@microsoft.com\"; result = await sp.web.ensureUser(username); ISiteUser \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers, SiteUser } from \"@pnp/sp/presets/all\"; Get user Groups \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let groups = await sp.web.currentUser.groups(); Add user to Site collection \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const user = await sp.web.ensureUser(\"userLoginname\") const users = await sp.web.siteUsers; await users.add(user.data.LoginName); Get user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // get user object by id const user = await sp.web.siteUsers.getById(6); //get user object by Email const user = await sp.web.siteUsers.getByEmail(\"user@mail.com\"); //get user object by LoginName const user = await sp.web.siteUsers.getByLoginName(\"userLoginName\"); Update user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let userProps = await sp.web.currentUser(); userProps.Title = \"New title\"; await sp.web.currentUser.update(userProps); Remove user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // remove user by id await sp.web.siteUsers.removeById(6); // remove user by LoginName await sp.web.siteUsers.removeByLoginName(6); ISiteUserProps \u00b6 User properties: Property Name Type Description Email string Contains Site user email Id Number Contains Site user Id IsHiddenInUI Boolean Site user IsHiddenInUI IsShareByEmailGuestUser boolean Site user is external user IsSiteAdmin Boolean Describes if Site user Is Site Admin LoginName string Site user LoginName PrincipalType number Site user Principal type Title string Site user Title interface ISiteUserProps { /** * Contains Site user email * */ Email: string; /** * Contains Site user Id * */ Id: number; /** * Site user IsHiddenInUI * */ IsHiddenInUI: boolean; /** * Site user IsShareByEmailGuestUser * */ IsShareByEmailGuestUser: boolean; /** * Describes if Site user Is Site Admin * */ IsSiteAdmin: boolean; /** * Site user LoginName * */ LoginName: string; /** * Site user Principal type * */ PrincipalType: number | PrincipalType; /** * Site user Title * */ Title: string; }","title":"Site Users"},{"location":"sp/site-users/#pnpspsite-users","text":"The site users module provides methods to manage users for a sharepoint site.","title":"@pnp/sp/site-users"},{"location":"sp/site-users/#isiteusers","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers } from \"@pnp/sp/presets/all\";","title":"ISiteUsers"},{"location":"sp/site-users/#get-all-site-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const users = await sp.web.siteUsers();","title":"Get all site user"},{"location":"sp/site-users/#get-current-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let user = await sp.web.currentUser();","title":"Get Current user"},{"location":"sp/site-users/#get-user-by-id","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const id = 6; user = await sp.web.getUserById(id);","title":"Get user by id"},{"location":"sp/site-users/#ensure-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const username = \"usernames@microsoft.com\"; result = await sp.web.ensureUser(username);","title":"Ensure user"},{"location":"sp/site-users/#isiteuser","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers, SiteUser } from \"@pnp/sp/presets/all\";","title":"ISiteUser"},{"location":"sp/site-users/#get-user-groups","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let groups = await sp.web.currentUser.groups();","title":"Get user Groups"},{"location":"sp/site-users/#add-user-to-site-collection","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const user = await sp.web.ensureUser(\"userLoginname\") const users = await sp.web.siteUsers; await users.add(user.data.LoginName);","title":"Add user to Site collection"},{"location":"sp/site-users/#get-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // get user object by id const user = await sp.web.siteUsers.getById(6); //get user object by Email const user = await sp.web.siteUsers.getByEmail(\"user@mail.com\"); //get user object by LoginName const user = await sp.web.siteUsers.getByLoginName(\"userLoginName\");","title":"Get user"},{"location":"sp/site-users/#update-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let userProps = await sp.web.currentUser(); userProps.Title = \"New title\"; await sp.web.currentUser.update(userProps);","title":"Update user"},{"location":"sp/site-users/#remove-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // remove user by id await sp.web.siteUsers.removeById(6); // remove user by LoginName await sp.web.siteUsers.removeByLoginName(6);","title":"Remove user"},{"location":"sp/site-users/#isiteuserprops","text":"User properties: Property Name Type Description Email string Contains Site user email Id Number Contains Site user Id IsHiddenInUI Boolean Site user IsHiddenInUI IsShareByEmailGuestUser boolean Site user is external user IsSiteAdmin Boolean Describes if Site user Is Site Admin LoginName string Site user LoginName PrincipalType number Site user Principal type Title string Site user Title interface ISiteUserProps { /** * Contains Site user email * */ Email: string; /** * Contains Site user Id * */ Id: number; /** * Site user IsHiddenInUI * */ IsHiddenInUI: boolean; /** * Site user IsShareByEmailGuestUser * */ IsShareByEmailGuestUser: boolean; /** * Describes if Site user Is Site Admin * */ IsSiteAdmin: boolean; /** * Site user LoginName * */ LoginName: string; /** * Site user Principal type * */ PrincipalType: number | PrincipalType; /** * Site user Title * */ Title: string; }","title":"ISiteUserProps"},{"location":"sp/sites/","text":"@pnp/sp/site - Site properties \u00b6 Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types. Get context information for the current site collection \u00b6 Using the library, you can get the context information of the current site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IContextInfo } from \"@pnp/sp/sites\"; const oContext: IContextInfo = await sp.site.getContextInfo(); console.log(oContext.FormDigestValue); Get document libraries of a web \u00b6 Using the library, you can get a list of the document libraries present in the a given web. Note: Works only in SharePoint online import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IDocumentLibraryInformation } from \"@pnp/sp/sites\"; const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries(\"https://tenant.sharepoint.com/sites/test/subsite\"); //we got the array of document library information docLibs.forEach((docLib: IDocumentLibraryInformation) => { // do something with each library }); Open Web By Id \u00b6 Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const w = await sp.site.openWebById(\"111ca453-90f5-482e-a381-cee1ff383c9e\"); //we got all the data from the web as well console.log(w.data); // we can chain const w2 = await w.web.select(\"Title\")(); Get site collection url from page \u00b6 Using the library, you can get the site collection url by providing a page url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const d: string = await sp.site.getWebUrlFromPageUrl(\"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\"); console.log(d); //https://tenant.sharepoint.com/sites/test Access the root web \u00b6 There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. \"_api/sites/rootweb\" which does not work for all operations. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; // use for rootweb information access const rootwebData = await sp.site.rootWeb(); // use for chaining const rootweb = await sp.site.getRootWeb(); const listData = await rootWeb.lists.getByTitle(\"MyList\")(); Create a modern communication site \u00b6 Note: Works only in SharePoint online Creates a modern communication site. Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection ) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site Owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createCommunicationSite( \"Title\", 1033, true, \"https://tenant.sharepoint.com/sites/commSite\", \"Description\", \"HBI\", \"f6cc5403-0d63-442e-96c0-285923709ffc\", \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"user@TENANT.onmicrosoft.com\"); Create from Props \u00b6 You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createCommunicationSiteFromProps({ Owner: \"patrick@three18studios.com\", Title: \"A Test Site\", Url: \"https://{tenant}.sharepoint.com/sites/commsite2\", WebTemplate: \"STS#3\", }); Create a modern team site \u00b6 Note: Works only in SharePoint online. It wont work with App only tokens Creates a modern team site backed by O365 group. Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createModernTeamSite( \"displayName\", \"alias\", true, 1033, \"description\", \"HBI\", [\"user1@tenant.onmicrosoft.com\",\"user2@tenant.onmicrosoft.com\",\"user3@tenant.onmicrosoft.com\"], \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"f6cc5403-0d63-442e-96c0-285923709ffc\" ); console.log(d); Create from Props \u00b6 You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createModernTeamSiteFromProps({ alias: \"JenniferGarner\", displayName: \"A Test Site\", owners: [\"patrick@three18studios.com\"], }); Delete a site collection \u00b6 Using the library, you can delete a specific site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { Site } from \"@pnp/sp/sites\"; // Delete the current site await sp.site.delete(); // Specify which site to delete const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const site2 = Site(siteUrl); await site2.delete(); Check if a Site Collection Exists \u00b6 Using the library, you can check if a specific site collection exist or not on your tenant import { sp } from \"@pnp/sp\"; // Specify which site to verify const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const exists = sp.site.exists(siteUrl); console.log(exists);","title":"Sites"},{"location":"sp/sites/#pnpspsite-site-properties","text":"Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.","title":"@pnp/sp/site - Site properties"},{"location":"sp/sites/#get-context-information-for-the-current-site-collection","text":"Using the library, you can get the context information of the current site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IContextInfo } from \"@pnp/sp/sites\"; const oContext: IContextInfo = await sp.site.getContextInfo(); console.log(oContext.FormDigestValue);","title":"Get context information for the current site collection"},{"location":"sp/sites/#get-document-libraries-of-a-web","text":"Using the library, you can get a list of the document libraries present in the a given web. Note: Works only in SharePoint online import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IDocumentLibraryInformation } from \"@pnp/sp/sites\"; const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries(\"https://tenant.sharepoint.com/sites/test/subsite\"); //we got the array of document library information docLibs.forEach((docLib: IDocumentLibraryInformation) => { // do something with each library });","title":"Get document libraries of a web"},{"location":"sp/sites/#open-web-by-id","text":"Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const w = await sp.site.openWebById(\"111ca453-90f5-482e-a381-cee1ff383c9e\"); //we got all the data from the web as well console.log(w.data); // we can chain const w2 = await w.web.select(\"Title\")();","title":"Open Web By Id"},{"location":"sp/sites/#get-site-collection-url-from-page","text":"Using the library, you can get the site collection url by providing a page url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const d: string = await sp.site.getWebUrlFromPageUrl(\"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\"); console.log(d); //https://tenant.sharepoint.com/sites/test","title":"Get site collection url from page"},{"location":"sp/sites/#access-the-root-web","text":"There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. \"_api/sites/rootweb\" which does not work for all operations. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; // use for rootweb information access const rootwebData = await sp.site.rootWeb(); // use for chaining const rootweb = await sp.site.getRootWeb(); const listData = await rootWeb.lists.getByTitle(\"MyList\")();","title":"Access the root web"},{"location":"sp/sites/#create-a-modern-communication-site","text":"Note: Works only in SharePoint online Creates a modern communication site. Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection ) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site Owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createCommunicationSite( \"Title\", 1033, true, \"https://tenant.sharepoint.com/sites/commSite\", \"Description\", \"HBI\", \"f6cc5403-0d63-442e-96c0-285923709ffc\", \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"user@TENANT.onmicrosoft.com\");","title":"Create a modern communication site"},{"location":"sp/sites/#create-from-props","text":"You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createCommunicationSiteFromProps({ Owner: \"patrick@three18studios.com\", Title: \"A Test Site\", Url: \"https://{tenant}.sharepoint.com/sites/commsite2\", WebTemplate: \"STS#3\", });","title":"Create from Props"},{"location":"sp/sites/#create-a-modern-team-site","text":"Note: Works only in SharePoint online. It wont work with App only tokens Creates a modern team site backed by O365 group. Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createModernTeamSite( \"displayName\", \"alias\", true, 1033, \"description\", \"HBI\", [\"user1@tenant.onmicrosoft.com\",\"user2@tenant.onmicrosoft.com\",\"user3@tenant.onmicrosoft.com\"], \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"f6cc5403-0d63-442e-96c0-285923709ffc\" ); console.log(d);","title":"Create a modern team site"},{"location":"sp/sites/#create-from-props_1","text":"You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createModernTeamSiteFromProps({ alias: \"JenniferGarner\", displayName: \"A Test Site\", owners: [\"patrick@three18studios.com\"], });","title":"Create from Props"},{"location":"sp/sites/#delete-a-site-collection","text":"Using the library, you can delete a specific site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { Site } from \"@pnp/sp/sites\"; // Delete the current site await sp.site.delete(); // Specify which site to delete const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const site2 = Site(siteUrl); await site2.delete();","title":"Delete a site collection"},{"location":"sp/sites/#check-if-a-site-collection-exists","text":"Using the library, you can check if a specific site collection exist or not on your tenant import { sp } from \"@pnp/sp\"; // Specify which site to verify const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const exists = sp.site.exists(siteUrl); console.log(exists);","title":"Check if a Site Collection Exists"},{"location":"sp/social/","text":"@pnp/sp/ - social \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions. getFollowedSitesUri \u00b6 Gets a URI to a site that lists the current user's followed sites. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedSitesUri(); getFollowedDocumentsUri \u00b6 Gets a URI to a site that lists the current user's followed documents. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedDocumentsUri(); follow \u00b6 Makes the current user start following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // follow a site const r1 = await sp.social.follow({ ActorType: SocialActorType.Site, ContentUri: \"htts://tenant.sharepoint.com/sites/site\", }); // follow a person const r2 = await sp.social.follow({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); // follow a doc const r3 = await sp.social.follow({ ActorType: SocialActorType.Document, ContentUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\", }); // follow a tag // You need the tag GUID to start following a tag. // You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. // See How to get a tag's GUID based on the tag's name by using the JavaScript object model. // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid const r4 = await sp.social.follow({ ActorType: SocialActorType.Tag, TagGuid: \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\", }); isFollowed \u00b6 Indicates whether the current user is following a specified user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.isFollowed({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); stopFollowing \u00b6 Makes the current user stop following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.stopFollowing({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); my \u00b6 get \u00b6 Gets this user's social information import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const r = await sp.social.my(); followed \u00b6 Gets users, documents, sites, and tags that the current user is following based on the supplied flags. import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get all the followed documents const r1 = await sp.social.my.followed(SocialActorTypes.Document); // get all the followed documents and sites const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site); // get all the followed sites updated in the last 24 hours const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours); followedCount \u00b6 Works as followed but returns on the count of actors specified by the query import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followedCount(SocialActorTypes.Document); followers \u00b6 Gets the users who are following the current user. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followers(); suggestions \u00b6 Gets users who the current user might want to follow. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.suggestions();","title":"Social"},{"location":"sp/social/#pnpsp-social","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions.","title":"@pnp/sp/ - social"},{"location":"sp/social/#getfollowedsitesuri","text":"Gets a URI to a site that lists the current user's followed sites. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedSitesUri();","title":"getFollowedSitesUri"},{"location":"sp/social/#getfolloweddocumentsuri","text":"Gets a URI to a site that lists the current user's followed documents. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedDocumentsUri();","title":"getFollowedDocumentsUri"},{"location":"sp/social/#follow","text":"Makes the current user start following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // follow a site const r1 = await sp.social.follow({ ActorType: SocialActorType.Site, ContentUri: \"htts://tenant.sharepoint.com/sites/site\", }); // follow a person const r2 = await sp.social.follow({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); // follow a doc const r3 = await sp.social.follow({ ActorType: SocialActorType.Document, ContentUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\", }); // follow a tag // You need the tag GUID to start following a tag. // You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. // See How to get a tag's GUID based on the tag's name by using the JavaScript object model. // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid const r4 = await sp.social.follow({ ActorType: SocialActorType.Tag, TagGuid: \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\", });","title":"follow"},{"location":"sp/social/#isfollowed","text":"Indicates whether the current user is following a specified user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.isFollowed({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, });","title":"isFollowed"},{"location":"sp/social/#stopfollowing","text":"Makes the current user stop following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.stopFollowing({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, });","title":"stopFollowing"},{"location":"sp/social/#my","text":"","title":"my"},{"location":"sp/social/#get","text":"Gets this user's social information import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const r = await sp.social.my();","title":"get"},{"location":"sp/social/#followed","text":"Gets users, documents, sites, and tags that the current user is following based on the supplied flags. import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get all the followed documents const r1 = await sp.social.my.followed(SocialActorTypes.Document); // get all the followed documents and sites const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site); // get all the followed sites updated in the last 24 hours const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);","title":"followed"},{"location":"sp/social/#followedcount","text":"Works as followed but returns on the count of actors specified by the query import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followedCount(SocialActorTypes.Document);","title":"followedCount"},{"location":"sp/social/#followers","text":"Gets the users who are following the current user. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followers();","title":"followers"},{"location":"sp/social/#suggestions","text":"Gets users who the current user might want to follow. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.suggestions();","title":"suggestions"},{"location":"sp/sp-utilities-utility/","text":"@pnp/sp/utilities \u00b6 Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching. sendEmail \u00b6 This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below). EmailProperties \u00b6 export interface EmailProperties { To: string[]; CC?: string[]; BCC?: string[]; Subject: string; Body: string; AdditionalHeaders?: TypedHash; From?: string; } Usage \u00b6 You must define the To, Subject, and Body values - the remaining are optional. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { IEmailProperties } from \"@pnp/sp/sputilities\"; const emailProps: IEmailProperties = { To: [\"user@site.com\"], CC: [\"user2@site.com\", \"user3@site.com\"], BCC: [\"user4@site.com\", \"user5@site.com\"], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" } }; await sp.utility.sendEmail(emailProps); console.log(\"Email Sent!\"); getCurrentUserEmailAddresses \u00b6 This method returns the current user's email addresses known to SharePoint. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let addressString: string = await sp.utility.getCurrentUserEmailAddresses(); // and use it with sendEmail await sp.utility.sendEmail({ To: [addressString], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" }, }); resolvePrincipal \u00b6 Gets information about a principal that matches the specified Search criteria import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principal : IPrincipalInfo = await sp.utility.resolvePrincipal(\"user@site.com\", PrincipalType.User, PrincipalSource.All, true, false, true); console.log(principal); searchPrincipals \u00b6 Gets information about the principals that match the specified Search criteria. import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals(\"john\", PrincipalType.User, PrincipalSource.All,\"\", 10); console.log(principals); createEmailBodyForInvitation \u00b6 Gets the external (outside the firewall) URL to a document or resource in a site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let url : string = await sp.utility.createEmailBodyForInvitation(\"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\"); console.log(url); expandGroupsToPrincipals \u00b6 Resolves the principals contained within the supplied groups import { sp, IPrincipalInfo } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"]); console.log(principals); // optionally supply a max results count. Default is 30. let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"], 10); console.log(principals); createWikiPage \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { ICreateWikiPageResult } from \"@pnp/sp/sputilities\"; let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({ ServerRelativeUrl: \"/sites/dev/SitePages/mynewpage.aspx\", WikiHtmlContent: \"This is my page content. It supports rich html.\", }); // newPage contains the raw data returned by the service console.log(newPage.data); // newPage contains a File instance you can use to further update the new page let file = await newPage.file(); console.log(file);","title":"SP.Utilities.Utility"},{"location":"sp/sp-utilities-utility/#pnpsputilities","text":"Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.","title":"@pnp/sp/utilities"},{"location":"sp/sp-utilities-utility/#sendemail","text":"This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).","title":"sendEmail"},{"location":"sp/sp-utilities-utility/#emailproperties","text":"export interface EmailProperties { To: string[]; CC?: string[]; BCC?: string[]; Subject: string; Body: string; AdditionalHeaders?: TypedHash; From?: string; }","title":"EmailProperties"},{"location":"sp/sp-utilities-utility/#usage","text":"You must define the To, Subject, and Body values - the remaining are optional. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { IEmailProperties } from \"@pnp/sp/sputilities\"; const emailProps: IEmailProperties = { To: [\"user@site.com\"], CC: [\"user2@site.com\", \"user3@site.com\"], BCC: [\"user4@site.com\", \"user5@site.com\"], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" } }; await sp.utility.sendEmail(emailProps); console.log(\"Email Sent!\");","title":"Usage"},{"location":"sp/sp-utilities-utility/#getcurrentuseremailaddresses","text":"This method returns the current user's email addresses known to SharePoint. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let addressString: string = await sp.utility.getCurrentUserEmailAddresses(); // and use it with sendEmail await sp.utility.sendEmail({ To: [addressString], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" }, });","title":"getCurrentUserEmailAddresses"},{"location":"sp/sp-utilities-utility/#resolveprincipal","text":"Gets information about a principal that matches the specified Search criteria import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principal : IPrincipalInfo = await sp.utility.resolvePrincipal(\"user@site.com\", PrincipalType.User, PrincipalSource.All, true, false, true); console.log(principal);","title":"resolvePrincipal"},{"location":"sp/sp-utilities-utility/#searchprincipals","text":"Gets information about the principals that match the specified Search criteria. import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals(\"john\", PrincipalType.User, PrincipalSource.All,\"\", 10); console.log(principals);","title":"searchPrincipals"},{"location":"sp/sp-utilities-utility/#createemailbodyforinvitation","text":"Gets the external (outside the firewall) URL to a document or resource in a site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let url : string = await sp.utility.createEmailBodyForInvitation(\"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\"); console.log(url);","title":"createEmailBodyForInvitation"},{"location":"sp/sp-utilities-utility/#expandgroupstoprincipals","text":"Resolves the principals contained within the supplied groups import { sp, IPrincipalInfo } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"]); console.log(principals); // optionally supply a max results count. Default is 30. let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"], 10); console.log(principals);","title":"expandGroupsToPrincipals"},{"location":"sp/sp-utilities-utility/#createwikipage","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { ICreateWikiPageResult } from \"@pnp/sp/sputilities\"; let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({ ServerRelativeUrl: \"/sites/dev/SitePages/mynewpage.aspx\", WikiHtmlContent: \"This is my page content. It supports rich html.\", }); // newPage contains the raw data returned by the service console.log(newPage.data); // newPage contains a File instance you can use to further update the new page let file = await newPage.file(); console.log(file);","title":"createWikiPage"},{"location":"sp/subscriptions/","text":"@pnp/sp/subscriptions \u00b6 Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library. ISubscriptions \u00b6 Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import {sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/presets/all\"; Add a webhook \u00b6 Using this library, you can add a webhook to a specified list within the SharePoint site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\"; // This is the URL which will be called by SharePoint when there is a change in the list const notificationUrl = \"\"; // Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date. const expiryDate = dateAdd(new Date(), \"day\" , 180).toISOString(); // Adds a webhook to the Documents library var res = await sp.web.lists.getByTitle(\"Documents\").subscriptions.add(notificationUrl,expiryDate); Get all webhooks added to a list \u00b6 Read all the webhooks' details which are associated to the list import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; const res = await sp.web.lists.getByTitle(\"Documents\").subscriptions(); ISubscription \u00b6 This interface provides the methods for managing a particular webhook. Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from \"@pnp/sp/presets/all\"; Managing a webhook \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; // Get details of a webhook based on its ID const webhookId = \"1f029e5c-16e4-4941-b46f-67895118763f\"; const webhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId)(); // Update a webhook const newDate = dateAdd(new Date(), \"day\" , 150).toISOString(); const updatedWebhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).update(newDate); // Delete a webhook await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).delete();","title":"Subscriptions"},{"location":"sp/subscriptions/#pnpspsubscriptions","text":"Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.","title":"@pnp/sp/subscriptions"},{"location":"sp/subscriptions/#isubscriptions","text":"Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import {sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/presets/all\";","title":"ISubscriptions"},{"location":"sp/subscriptions/#add-a-webhook","text":"Using this library, you can add a webhook to a specified list within the SharePoint site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\"; // This is the URL which will be called by SharePoint when there is a change in the list const notificationUrl = \"\"; // Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date. const expiryDate = dateAdd(new Date(), \"day\" , 180).toISOString(); // Adds a webhook to the Documents library var res = await sp.web.lists.getByTitle(\"Documents\").subscriptions.add(notificationUrl,expiryDate);","title":"Add a webhook"},{"location":"sp/subscriptions/#get-all-webhooks-added-to-a-list","text":"Read all the webhooks' details which are associated to the list import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; const res = await sp.web.lists.getByTitle(\"Documents\").subscriptions();","title":"Get all webhooks added to a list"},{"location":"sp/subscriptions/#isubscription","text":"This interface provides the methods for managing a particular webhook. Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from \"@pnp/sp/presets/all\";","title":"ISubscription"},{"location":"sp/subscriptions/#managing-a-webhook","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; // Get details of a webhook based on its ID const webhookId = \"1f029e5c-16e4-4941-b46f-67895118763f\"; const webhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId)(); // Update a webhook const newDate = dateAdd(new Date(), \"day\" , 150).toISOString(); const updatedWebhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).update(newDate); // Delete a webhook await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).delete();","title":"Managing a webhook"},{"location":"sp/taxonomy/","text":"@pnp/sp/taxonomy \u00b6 Provides access to the v2.1 api term store Docs updated with v2.0.9 release as the underlying API changed. \u00b6 NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabalize this note will be removed. Term Store \u00b6 Access term store data from the root sp object as shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermStoreInfo } from \"@pnp/sp/taxonomy\"; // get term store data const info: ITermStoreInfo = await sp.termStore(); Term Groups \u00b6 Access term group information List \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups const info: ITermGroupInfo[] = await sp.termStore.groups(); Get By Id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups data const info: ITermGroupInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\")(); Term Sets \u00b6 Access term set information List \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get get set info const info: ITermSetInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets(); Get By Id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermSetInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")(); getAllChildrenAsOrderedTree \u00b6 Added in 2.0.13 This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; import { dateAdd, PnPClientStorage } from \"@pnp/core\"; // here we get all the children of a given set const childTree = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); // here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available const store = new PnPClientStorage(); // our tree likely doesn't change much in 30 minutes for most applications // adjust to be longer or shorter as needed const cachedTree = await store.local.getOrPut(\"myKey\", () => { return sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); }, dateAdd(new Date(), \"minute\", 30)); Terms \u00b6 Access term set information List \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children(); List (terms) \u00b6 Added in 2.0.13 You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms(); Get By Id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\")(); Get Term Parent \u00b6 Behavior Change in 2.1.0 The server API changed again, resulting in the removal of the \"parent\" property from ITerm as it is not longer supported as a path property. You now must use \"expand\" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; // get a ref to the set const set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\"); // get a term's information and expand parent to get the parent info as well const w = await set.getTermById(\"338666a8-1111-2222-3333-f72471314e72\").expand(\"parent\")(); // get a ref to the parent term const parent = set.getTermById(w.parent.id); // make a request for the parent term's info - this data currently match the results in the expand call above, but this // is to demonstrate how to gain a ref to the parent and select its data const parentInfo = await parent.select(\"Id\", \"Descriptions\")();","title":"Taxonomy"},{"location":"sp/taxonomy/#pnpsptaxonomy","text":"Provides access to the v2.1 api term store","title":"@pnp/sp/taxonomy"},{"location":"sp/taxonomy/#docs-updated-with-v209-release-as-the-underlying-api-changed","text":"NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabalize this note will be removed.","title":"Docs updated with v2.0.9 release as the underlying API changed."},{"location":"sp/taxonomy/#term-store","text":"Access term store data from the root sp object as shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermStoreInfo } from \"@pnp/sp/taxonomy\"; // get term store data const info: ITermStoreInfo = await sp.termStore();","title":"Term Store"},{"location":"sp/taxonomy/#term-groups","text":"Access term group information","title":"Term Groups"},{"location":"sp/taxonomy/#list","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups const info: ITermGroupInfo[] = await sp.termStore.groups();","title":"List"},{"location":"sp/taxonomy/#get-by-id","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups data const info: ITermGroupInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\")();","title":"Get By Id"},{"location":"sp/taxonomy/#term-sets","text":"Access term set information","title":"Term Sets"},{"location":"sp/taxonomy/#list_1","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get get set info const info: ITermSetInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets();","title":"List"},{"location":"sp/taxonomy/#get-by-id_1","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermSetInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")();","title":"Get By Id"},{"location":"sp/taxonomy/#getallchildrenasorderedtree","text":"Added in 2.0.13 This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; import { dateAdd, PnPClientStorage } from \"@pnp/core\"; // here we get all the children of a given set const childTree = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); // here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available const store = new PnPClientStorage(); // our tree likely doesn't change much in 30 minutes for most applications // adjust to be longer or shorter as needed const cachedTree = await store.local.getOrPut(\"myKey\", () => { return sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); }, dateAdd(new Date(), \"minute\", 30));","title":"getAllChildrenAsOrderedTree"},{"location":"sp/taxonomy/#terms","text":"Access term set information","title":"Terms"},{"location":"sp/taxonomy/#list_2","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children();","title":"List"},{"location":"sp/taxonomy/#list-terms","text":"Added in 2.0.13 You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms();","title":"List (terms)"},{"location":"sp/taxonomy/#get-by-id_2","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\")();","title":"Get By Id"},{"location":"sp/taxonomy/#get-term-parent","text":"Behavior Change in 2.1.0 The server API changed again, resulting in the removal of the \"parent\" property from ITerm as it is not longer supported as a path property. You now must use \"expand\" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; // get a ref to the set const set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\"); // get a term's information and expand parent to get the parent info as well const w = await set.getTermById(\"338666a8-1111-2222-3333-f72471314e72\").expand(\"parent\")(); // get a ref to the parent term const parent = set.getTermById(w.parent.id); // make a request for the parent term's info - this data currently match the results in the expand call above, but this // is to demonstrate how to gain a ref to the parent and select its data const parentInfo = await parent.select(\"Id\", \"Descriptions\")();","title":"Get Term Parent"},{"location":"sp/tenant-properties/","text":"@pnp/sp/web - tenant properties \u00b6 You can set, read, and remove tenant properties using the methods shown below: setStorageEntity \u00b6 This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); // specify required key and value await w.setStorageEntity(\"Test1\", \"Value 1\"); // specify optional description and comments await w.setStorageEntity(\"Test2\", \"Value 2\", \"description\", \"comments\"); getStorageEntity \u00b6 This method can be used from any web to retrieve values previously set. import { sp, IStorageEntity } from \"@pnp/sp/presets/all\"; const prop: IStorageEntity = await sp.web.getStorageEntity(\"Test1\"); console.log(prop.Value); removeStorageEntity \u00b6 This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); await w.removeStorageEntity(\"Test1\");","title":"Tenant Properties"},{"location":"sp/tenant-properties/#pnpspweb-tenant-properties","text":"You can set, read, and remove tenant properties using the methods shown below:","title":"@pnp/sp/web - tenant properties"},{"location":"sp/tenant-properties/#setstorageentity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); // specify required key and value await w.setStorageEntity(\"Test1\", \"Value 1\"); // specify optional description and comments await w.setStorageEntity(\"Test2\", \"Value 2\", \"description\", \"comments\");","title":"setStorageEntity"},{"location":"sp/tenant-properties/#getstorageentity","text":"This method can be used from any web to retrieve values previously set. import { sp, IStorageEntity } from \"@pnp/sp/presets/all\"; const prop: IStorageEntity = await sp.web.getStorageEntity(\"Test1\"); console.log(prop.Value);","title":"getStorageEntity"},{"location":"sp/tenant-properties/#removestorageentity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); await w.removeStorageEntity(\"Test1\");","title":"removeStorageEntity"},{"location":"sp/user-custom-actions/","text":"@pnp/sp/user-custom-actions \u00b6 Represents a custom action associated with a SharePoint list, web or site collection. IUserCustomActions \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IUserCustomActions, IUserCustomAction } from \"@pnp/sp/user-custom-actions\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/user-custom-actions\"; Preset: All import { sp, IUserCustomActions, IUserCustomAction } from \"@pnp/sp/presents/all\"; Get a collection of User Custom Actions from a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const userCustomActions = sp.web.userCustomActions(); Add a new User Custom Action \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions'; const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"Location\": \"ScriptLink\", \"ScriptSrc\": \"https://...\" }; const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues); Get a User Custom Action by ID \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const uca: IUserCustomAction = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const ucaData = await uca(); Clear the User Custom Action collection \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; // Site collection level await sp.site.userCustomActions.clear(); // Site (web) level await sp.web.userCustomActions.clear(); // List level await sp.web.lists.getByTitle(\"Documents\").userCustomActions.clear(); IUserCustomAction \u00b6 Update an existing User Custom Action \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions'; const uca = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"ScriptSrc\": \"https://...\" }; const response: IUserCustomActionUpdateResult = uca.update(newValues);","title":"User custom actions"},{"location":"sp/user-custom-actions/#pnpspuser-custom-actions","text":"Represents a custom action associated with a SharePoint list, web or site collection.","title":"@pnp/sp/user-custom-actions"},{"location":"sp/user-custom-actions/#iusercustomactions","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IUserCustomActions, IUserCustomAction } from \"@pnp/sp/user-custom-actions\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/user-custom-actions\"; Preset: All import { sp, IUserCustomActions, IUserCustomAction } from \"@pnp/sp/presents/all\";","title":"IUserCustomActions"},{"location":"sp/user-custom-actions/#get-a-collection-of-user-custom-actions-from-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const userCustomActions = sp.web.userCustomActions();","title":"Get a collection of User Custom Actions from a web"},{"location":"sp/user-custom-actions/#add-a-new-user-custom-action","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions'; const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"Location\": \"ScriptLink\", \"ScriptSrc\": \"https://...\" }; const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);","title":"Add a new User Custom Action"},{"location":"sp/user-custom-actions/#get-a-user-custom-action-by-id","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const uca: IUserCustomAction = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const ucaData = await uca();","title":"Get a User Custom Action by ID"},{"location":"sp/user-custom-actions/#clear-the-user-custom-action-collection","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; // Site collection level await sp.site.userCustomActions.clear(); // Site (web) level await sp.web.userCustomActions.clear(); // List level await sp.web.lists.getByTitle(\"Documents\").userCustomActions.clear();","title":"Clear the User Custom Action collection"},{"location":"sp/user-custom-actions/#iusercustomaction","text":"","title":"IUserCustomAction"},{"location":"sp/user-custom-actions/#update-an-existing-user-custom-action","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions'; const uca = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"ScriptSrc\": \"https://...\" }; const response: IUserCustomActionUpdateResult = uca.update(newValues);","title":"Update an existing User Custom Action"},{"location":"sp/views/","text":"@pnp/sp/views \u00b6 Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view. IViews \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Views, IViews } from \"@pnp/sp/views\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/views\"; Preset: All import { sp, Views, IViews } from \"@pnp/sp/presets/all\"; Get views in a list \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // get all the views and their properties const views1 = await list.views(); // you can use odata select operations to get just a set a fields const views2 = await list.views.select(\"Id\", \"Title\")(); // get the top three views const views3 = await list.views.top(3)(); Add a View \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // create a new view with default fields and properties const result = await list.views.add(\"My New View\"); // create a new view with specific properties const result2 = await list.views.add(\"My New View 2\", false, { RowLimit: 10, ViewQuery: \"\", }); // manipulate the view's fields await result2.view.fields.removeAll(); await Promise.all([ result2.view.fields.add(\"Title\"), result2.view.fields.add(\"Modified\"), ]); IView \u00b6 Get a View's Information \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\")(); const result2 = await list.views.getByTitle(\"My View\")(); const result3 = await list.views.getByTitle(\"My View\").select(\"Id\", \"Title\")(); const result4 = await list.defaultView(); const result5 = await list.getView(\"{GUID view id}\")(); fields \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").fields(); update \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").update({ RowLimit: 20, }); renderAsHtml \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const result = await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").renderAsHtml(); setViewXml \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").setViewXml(viewXml); delete \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").delete(); ViewFields \u00b6 getSchemaXml \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const xml = await sp.web.lists.getByTitle(\"My List\").defaultView.fields.getSchemaXml(); add \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.add(\"Created\"); move \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.move(\"Created\", 0); remove \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.remove(\"Created\"); removeAll \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.removeAll();","title":"Views"},{"location":"sp/views/#pnpspviews","text":"Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.","title":"@pnp/sp/views"},{"location":"sp/views/#iviews","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Views, IViews } from \"@pnp/sp/views\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/views\"; Preset: All import { sp, Views, IViews } from \"@pnp/sp/presets/all\";","title":"IViews"},{"location":"sp/views/#get-views-in-a-list","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // get all the views and their properties const views1 = await list.views(); // you can use odata select operations to get just a set a fields const views2 = await list.views.select(\"Id\", \"Title\")(); // get the top three views const views3 = await list.views.top(3)();","title":"Get views in a list"},{"location":"sp/views/#add-a-view","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // create a new view with default fields and properties const result = await list.views.add(\"My New View\"); // create a new view with specific properties const result2 = await list.views.add(\"My New View 2\", false, { RowLimit: 10, ViewQuery: \"\", }); // manipulate the view's fields await result2.view.fields.removeAll(); await Promise.all([ result2.view.fields.add(\"Title\"), result2.view.fields.add(\"Modified\"), ]);","title":"Add a View"},{"location":"sp/views/#iview","text":"","title":"IView"},{"location":"sp/views/#get-a-views-information","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\")(); const result2 = await list.views.getByTitle(\"My View\")(); const result3 = await list.views.getByTitle(\"My View\").select(\"Id\", \"Title\")(); const result4 = await list.defaultView(); const result5 = await list.getView(\"{GUID view id}\")();","title":"Get a View's Information"},{"location":"sp/views/#fields","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").fields();","title":"fields"},{"location":"sp/views/#update","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").update({ RowLimit: 20, });","title":"update"},{"location":"sp/views/#renderashtml","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const result = await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").renderAsHtml();","title":"renderAsHtml"},{"location":"sp/views/#setviewxml","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").setViewXml(viewXml);","title":"setViewXml"},{"location":"sp/views/#delete","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").delete();","title":"delete"},{"location":"sp/views/#viewfields","text":"","title":"ViewFields"},{"location":"sp/views/#getschemaxml","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const xml = await sp.web.lists.getByTitle(\"My List\").defaultView.fields.getSchemaXml();","title":"getSchemaXml"},{"location":"sp/views/#add","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.add(\"Created\");","title":"add"},{"location":"sp/views/#move","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.move(\"Created\", 0);","title":"move"},{"location":"sp/views/#remove","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.remove(\"Created\");","title":"remove"},{"location":"sp/views/#removeall","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.removeAll();","title":"removeAll"},{"location":"sp/webs/","text":"@pnp/sp/webs \u00b6 Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types. IWebs \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Webs, IWebs } from \"@pnp/sp/presets/core\"; Add Web \u00b6 Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions. import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; const result = await sp.web.webs.add(\"title\", \"subweb1\"); // show the response from the server when adding the web console.log(result.data); // we can immediately operate on the new web result.web.select(\"Title\")().then((w: IWebAddResult) => { // show our title console.log(w.Title); }); import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; // create a German language wiki site with title, url, description, which does not inherit permissions sp.web.webs.add(\"wiki\", \"subweb2\", \"a wiki web\", \"WIKI#0\", 1031, false).then((w: IWebAddResult) => { // ... }); IWeb \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Web, IWeb } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Web, IWeb } from \"@pnp/sp/presets/core\"; Access a Web \u00b6 There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named \"web\" which represents an IWeb instance - regardless of how it was initially accessed. Access the web from the imported \"sp\" object using selective import: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'all' preset import { sp } from \"@pnp/sp/presets/all\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'core' preset import { sp } from \"@pnp/sp/presets/core\"; const r = await sp.web(); Create a web instance using the factory function import { Web } from \"@pnp/sp/webs\"; const web = Web(\"https://something.sharepoint.com/sites/dev\"); const r = await web(); webs \u00b6 Access the child webs collection of this web const webs = web.webs(); Get A Web's properties \u00b6 // basic get of the webs properties const props = await web(); // use odata operators to get specific fields const props2 = await web.select(\"Title\")(); // type the result to match what you are requesting const props3 = await web.select(\"Title\")<{ Title: string }>(); getParentWeb \u00b6 Get the data and IWeb instance for the parent web for the given web instance import { IOpenWebByIdResult } from \"@pnp/sp/sites\"; const web: IOpenWebByIdResult = web.getParentWeb(); getSubwebsFilteredForCurrentUser \u00b6 Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member. const subWebs = await web.getSubwebsFilteredForCurrentUser()(); // apply odata operations to the collection const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select(\"Title\", \"Language\").orderBy(\"Created\", true)(); Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo. allProperties \u00b6 Allows access to the web's all properties collection. This is readonly in REST. const props = await web.allProperties(); // select certain props const props2 = await web.allProperties.select(\"prop1\", \"prop2\")(); webinfos \u00b6 Gets a collection of WebInfos for this web's subwebs const infos = await web.webinfos(); // or select certain fields const infos2 = await web.webinfos.select(\"Title\", \"Description\")(); // or filter const infos3 = await web.webinfos.filter(\"Title eq 'MyWebTitle'\")(); // or both const infos4 = await web.webinfos.select(\"Title\", \"Description\").filter(\"Title eq 'MyWebTitle'\")(); // get the top 4 ordered by Title const infos5 = await web.webinfos.top(4).orderBy(\"Title\")(); Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo. update \u00b6 Updates this web instance with the supplied properties // update the web's title and description const result = await web.update({ Title: \"New Title\", Description: \"My new description\", }); // a project implementation could wrap the update to provide type information for your expected fields: import { IWebUpdateResult } from \"@pnp/sp/webs\"; interface IWebUpdateProps { Title: string; Description: string; } function updateWeb(props: IWebUpdateProps): Promise { web.update(props); } Delete a Web \u00b6 await web.delete(); applyTheme \u00b6 Applies the theme specified by the contents of each of the files specified in the arguments to the site import { combine } from \"@pnp/core\"; // we are going to apply the theme to this sub web as an example const web = Web(\"https://{tenant}.sharepoint.com/sites/dev/subweb\"); // the urls to the color and font need to both be from the catalog at the root // these urls can be constants or calculated from existing urls const colorUrl = combine(\"/\", \"sites/dev\", \"_catalogs/theme/15/palette011.spcolor\"); // this gives us the same result const fontUrl = \"/sites/dev/_catalogs/theme/15/fontscheme007.spfont\"; // apply the font and color, no background image, and don't share this theme await web.applyTheme(colorUrl, fontUrl, \"\", false); applyWebTemplate & availableWebTemplates \u00b6 Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios. const templates = (await web.availableWebTemplates().select(\"Name\")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name)); // apply the wiki template const template = templates.length > 0 ? templates[0].Name : \"STS#0\"; await web.applyWebTemplate(template); getChanges \u00b6 Returns the collection of changes from the change log that have occurred within the web, based on the specified query. // get the web changes including add, update, and delete const changes = await web.getChanges({ Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Update: true, Web: true, }); mapToIcon \u00b6 Returns the name of the image file for the icon that is used to represent the specified file import { combine } from \"@pnp/core\"; const iconFileName = await web.mapToIcon(\"test.docx\"); // iconPath === \"icdocx.png\" // which you can need to map to a real url const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`; // OR dynamically const webData = await sp.web.select(\"Url\")(); const iconFullPath2 = combine(webData.Url, \"_layouts\", \"images\", iconFileName); // OR within SPFx using the context const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, \"_layouts\", \"images\", iconFileName); // You can also set size // 16x16 pixels = 0, 32x32 pixels = 1 const icon32FileName = await web.mapToIcon(\"test.docx\", 1); storage entities \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import { IStorageEntity } from \"@pnp/sp/webs\"; // needs to be unique, GUIDs are great const key = \"my-storage-key\"; // read an existing entity const entity: IStorageEntity = await web.getStorageEntity(key); // setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site // you can get the tenant app catalog using the getTenantAppCatalogWeb const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb(); tenantAppCatalogWeb.setStorageEntity(key, \"new value\"); // set other properties tenantAppCatalogWeb.setStorageEntity(key, \"another value\", \"description\", \"comments\"); const entity2: IStorageEntity = await web.getStorageEntity(key); /* entity2 === { Value: \"another value\", Comment: \"comments\"; Description: \"description\", }; */ // you can also remove a storage entity await tenantAppCatalogWeb.removeStorageEntity(key); appcatalog imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/appcatalog\"; Selective 2 import \"@pnp/sp/appcatalog/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; getAppCatalog \u00b6 Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url. import { IApp } from \"@pnp/sp/appcatalog\"; const appWeb = web.getAppCatalog(); // appWeb url === web url const app: IApp = appWeb.getAppById(\"{your app id}\"); const appWeb2 = web.getAppCatalog(\"https://tenant.sharepoing.com/sites/someappcatalog\"); // appWeb2 url === \"https://tenant.sharepoing.com/sites/someappcatalog\" client-side-pages imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/client-side-pages\"; Selective 2 import \"@pnp/sp/client-side-pages/web\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // simplest add a page example const page = await sp.web.addClientsidePage(\"mypage1\"); // simplest load a page example const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); content-type imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; contentTypes \u00b6 Allows access to the collection of content types in this web. const cts = await web.contentTypes(); // you can also select fields and use other odata operators const cts2 = await web.contentTypes.select(\"Name\")(); features imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/features\"; Selective 2 import \"@pnp/sp/features/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; features \u00b6 Allows access to the collection of content types in this web. const features = await web.features(); fields imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; fields \u00b6 Allows access to the collection of fields in this web. const fields = await web.fields(); files imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/files\"; Selective 2 import \"@pnp/sp/files/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; getFileByServerRelativeUrl \u00b6 Gets a file by server relative url import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativeUrl(\"/sites/dev/library/myfile.docx\"); getFileByServerRelativePath \u00b6 Gets a file by server relative url if your file name contains # and % characters import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativePath(\"/sites/dev/library/my # file%.docx\"); folders imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; folders \u00b6 Gets the collection of folders in this web const folders = await web.folders(); // you can also filter and select as with any collection const folders2 = await web.folders.select(\"ServerRelativeUrl\", \"TimeLastModified\").filter(\"ItemCount gt 0\")(); // or get the most recently modified folder const folders2 = await web.folders.orderBy(\"TimeLastModified\").top(1)(); rootFolder \u00b6 Gets the root folder of the web const folder = await web.rootFolder(); getFolderByServerRelativeUrl \u00b6 Gets a folder by server relative url import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativeUrl(\"/sites/dev/library\"); getFolderByServerRelativePath \u00b6 Gets a folder by server relative url if your folder name contains # and % characters import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativePath(\"/sites/dev/library/my # folder%/\"); hubsites imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/hubsites\"; Selective 2 import \"@pnp/sp/hubsites/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; hubSiteData \u00b6 Gets hub site data for the current web import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; // get the data and force a refresh const data: IHubSiteWebData = await web.hubSiteData(true); syncHubSiteTheme \u00b6 Applies theme updates from the parent hub site collection await web.syncHubSiteTheme(); lists imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/lists\"; Selective 2 import \"@pnp/sp/lists/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\"; lists \u00b6 Gets the collection of all lists that are contained in the Web site import { ILists } from \"@pnp/sp/lists\"; const lists: ILists = web.lists; // you can always order the lists and select properties const data = await lists.select(\"Title\").orderBy(\"Title\")(); // and use other odata operators as well const data2 = await web.lists.top(3).orderBy(\"LastItemModifiedDate\")(); siteUserInfoList \u00b6 Gets the UserInfo list of the site collection that contains the Web site import { IList } from \"@pnp/sp/lists\"; const list: IList = web.siteUserInfoList; const data = await list(); // or chain off that list to get additional details const items = await list.items.top(2)(); defaultDocumentLibrary \u00b6 Get a reference the default documents library of a web import { IList } from \"@pnp/sp/lists\"; const list: IList = web.defaultDocumentLibrary; customListTemplates \u00b6 Gets the collection of all list definitions and list templates that are available import { IList } from \"@pnp/sp/lists\"; const templates = await web.customListTemplates(); // odata operators chain off the collection as expected const templates2 = await web.customListTemplates.select(\"Title\")(); getList \u00b6 Gets a list by server relative url (list's root folder) import { IList } from \"@pnp/sp/lists\"; const list: IList = web.getList(\"/sites/dev/lists/test\"); const listData = list(); getCatalog \u00b6 Returns the list gallery on the site Name Value WebTemplateCatalog 111 WebPartCatalog 113 ListTemplateCatalog 114 MasterPageCatalog 116 SolutionCatalog 121 ThemeCatalog 123 DesignCatalog 124 AppDataCatalog 125 import { IList } from \"@pnp/sp/lists\"; const templateCatalog: IList = await web.getCatalog(111); const themeCatalog: IList = await web.getCatalog(123); navigation imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/navigation\"; Selective 2 import \"@pnp/sp/navigation/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; navigation \u00b6 Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar import { INavigation } from \"@pnp/sp/navigation\"; const nav: INavigation = web.navigation; const navData = await nav(); regional-settings imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/regional-settings\"; Selective 2 import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRegionalSettings } from \"@pnp/sp/navigation\"; const settings: IRegionalSettings = web.regionalSettings; const settingsData = await settings(); related-items imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/related-items\"; Selective 2 import \"@pnp/sp/related-items/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRelatedItemManager, IRelatedItem } from \"@pnp/sp/related-items\"; const manager: IRelatedItemManager = web.relatedItems; const data: IRelatedItem[] = await manager.getRelatedItems(\"{list name}\", 4); security imports \u00b6 Please see information around the available security methods in the security article . sharing imports \u00b6 Please see information around the available sharing methods in the sharing article . site-groups imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/site-groups\"; Selective 2 import \"@pnp/sp/site-groups/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; siteGroups \u00b6 The site groups const groups = await web.siteGroups(); const groups2 = await web.siteGroups.top(2)(); associatedOwnerGroup \u00b6 The web's owner group const group = await web.associatedOwnerGroup(); const users = await web.associatedOwnerGroup.users(); associatedMemberGroup \u00b6 The web's member group const group = await web.associatedMemberGroup(); const users = await web.associatedMemberGroup.users(); associatedVisitorGroup \u00b6 The web's visitor group const group = await web.associatedVisitorGroup(); const users = await web.associatedVisitorGroup.users(); createDefaultAssociatedGroups \u00b6 Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\"); // copy the role assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", true); // don't clear sub assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, false); // specify secondary owner, don't copy permissions, clear sub scopes await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, true, \"{second owner login}\"); site-users imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/site-users\"; Selective 2 import \"@pnp/sp/site-users/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; siteUsers \u00b6 The site users const users = await web.siteUsers(); const users2 = await web.siteUsers.top(5)(); const users3 = await web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent(\"i:0#.f|m\")}')`)(); currentUser \u00b6 Information on the current user const user = await web.currentUser(); // check the login name of the current user const user2 = await web.currentUser.select(\"LoginName\")(); ensureUser \u00b6 Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web import { IWebEnsureUserResult } from \"@pnp/sp/site-users/\"; const result: IWebEnsureUserResult = await web.ensureUser(\"i:0#.f|membership|user@domain.onmicrosoft.com\"); getUserById \u00b6 Returns the user corresponding to the specified member identifier for the current web import { ISiteUser } from \"@pnp/sp/site-users/\"; const user: ISiteUser = web.getUserById(23); const userData = await user(); const userData2 = await user.select(\"LoginName\")(); user-custom-actions imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; userCustomActions \u00b6 Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection import { IUserCustomActions } from \"@pnp/sp/user-custom-actions\"; const actions: IUserCustomActions = web.userCustomActions; const actionsData = await actions(); IWebInfosData \u00b6 Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations. interface IWebInfosData { Configuration: number; Created: string; Description: string; Id: string; Language: number; LastItemModifiedDate: string; LastItemUserModifiedDate: string; ServerRelativeUrl: string; Title: string; WebTemplate: string; WebTemplateId: number; }","title":"Webs"},{"location":"sp/webs/#pnpspwebs","text":"Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.","title":"@pnp/sp/webs"},{"location":"sp/webs/#iwebs","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Webs, IWebs } from \"@pnp/sp/presets/core\";","title":"IWebs"},{"location":"sp/webs/#add-web","text":"Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions. import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; const result = await sp.web.webs.add(\"title\", \"subweb1\"); // show the response from the server when adding the web console.log(result.data); // we can immediately operate on the new web result.web.select(\"Title\")().then((w: IWebAddResult) => { // show our title console.log(w.Title); }); import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; // create a German language wiki site with title, url, description, which does not inherit permissions sp.web.webs.add(\"wiki\", \"subweb2\", \"a wiki web\", \"WIKI#0\", 1031, false).then((w: IWebAddResult) => { // ... });","title":"Add Web"},{"location":"sp/webs/#iweb","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Web, IWeb } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Web, IWeb } from \"@pnp/sp/presets/core\";","title":"IWeb"},{"location":"sp/webs/#access-a-web","text":"There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named \"web\" which represents an IWeb instance - regardless of how it was initially accessed. Access the web from the imported \"sp\" object using selective import: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'all' preset import { sp } from \"@pnp/sp/presets/all\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'core' preset import { sp } from \"@pnp/sp/presets/core\"; const r = await sp.web(); Create a web instance using the factory function import { Web } from \"@pnp/sp/webs\"; const web = Web(\"https://something.sharepoint.com/sites/dev\"); const r = await web();","title":"Access a Web"},{"location":"sp/webs/#webs","text":"Access the child webs collection of this web const webs = web.webs();","title":"webs"},{"location":"sp/webs/#get-a-webs-properties","text":"// basic get of the webs properties const props = await web(); // use odata operators to get specific fields const props2 = await web.select(\"Title\")(); // type the result to match what you are requesting const props3 = await web.select(\"Title\")<{ Title: string }>();","title":"Get A Web's properties"},{"location":"sp/webs/#getparentweb","text":"Get the data and IWeb instance for the parent web for the given web instance import { IOpenWebByIdResult } from \"@pnp/sp/sites\"; const web: IOpenWebByIdResult = web.getParentWeb();","title":"getParentWeb"},{"location":"sp/webs/#getsubwebsfilteredforcurrentuser","text":"Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member. const subWebs = await web.getSubwebsFilteredForCurrentUser()(); // apply odata operations to the collection const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select(\"Title\", \"Language\").orderBy(\"Created\", true)(); Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.","title":"getSubwebsFilteredForCurrentUser"},{"location":"sp/webs/#allproperties","text":"Allows access to the web's all properties collection. This is readonly in REST. const props = await web.allProperties(); // select certain props const props2 = await web.allProperties.select(\"prop1\", \"prop2\")();","title":"allProperties"},{"location":"sp/webs/#webinfos","text":"Gets a collection of WebInfos for this web's subwebs const infos = await web.webinfos(); // or select certain fields const infos2 = await web.webinfos.select(\"Title\", \"Description\")(); // or filter const infos3 = await web.webinfos.filter(\"Title eq 'MyWebTitle'\")(); // or both const infos4 = await web.webinfos.select(\"Title\", \"Description\").filter(\"Title eq 'MyWebTitle'\")(); // get the top 4 ordered by Title const infos5 = await web.webinfos.top(4).orderBy(\"Title\")(); Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.","title":"webinfos"},{"location":"sp/webs/#update","text":"Updates this web instance with the supplied properties // update the web's title and description const result = await web.update({ Title: \"New Title\", Description: \"My new description\", }); // a project implementation could wrap the update to provide type information for your expected fields: import { IWebUpdateResult } from \"@pnp/sp/webs\"; interface IWebUpdateProps { Title: string; Description: string; } function updateWeb(props: IWebUpdateProps): Promise { web.update(props); }","title":"update"},{"location":"sp/webs/#delete-a-web","text":"await web.delete();","title":"Delete a Web"},{"location":"sp/webs/#applytheme","text":"Applies the theme specified by the contents of each of the files specified in the arguments to the site import { combine } from \"@pnp/core\"; // we are going to apply the theme to this sub web as an example const web = Web(\"https://{tenant}.sharepoint.com/sites/dev/subweb\"); // the urls to the color and font need to both be from the catalog at the root // these urls can be constants or calculated from existing urls const colorUrl = combine(\"/\", \"sites/dev\", \"_catalogs/theme/15/palette011.spcolor\"); // this gives us the same result const fontUrl = \"/sites/dev/_catalogs/theme/15/fontscheme007.spfont\"; // apply the font and color, no background image, and don't share this theme await web.applyTheme(colorUrl, fontUrl, \"\", false);","title":"applyTheme"},{"location":"sp/webs/#applywebtemplate-availablewebtemplates","text":"Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios. const templates = (await web.availableWebTemplates().select(\"Name\")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name)); // apply the wiki template const template = templates.length > 0 ? templates[0].Name : \"STS#0\"; await web.applyWebTemplate(template);","title":"applyWebTemplate & availableWebTemplates"},{"location":"sp/webs/#getchanges","text":"Returns the collection of changes from the change log that have occurred within the web, based on the specified query. // get the web changes including add, update, and delete const changes = await web.getChanges({ Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Update: true, Web: true, });","title":"getChanges"},{"location":"sp/webs/#maptoicon","text":"Returns the name of the image file for the icon that is used to represent the specified file import { combine } from \"@pnp/core\"; const iconFileName = await web.mapToIcon(\"test.docx\"); // iconPath === \"icdocx.png\" // which you can need to map to a real url const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`; // OR dynamically const webData = await sp.web.select(\"Url\")(); const iconFullPath2 = combine(webData.Url, \"_layouts\", \"images\", iconFileName); // OR within SPFx using the context const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, \"_layouts\", \"images\", iconFileName); // You can also set size // 16x16 pixels = 0, 32x32 pixels = 1 const icon32FileName = await web.mapToIcon(\"test.docx\", 1);","title":"mapToIcon"},{"location":"sp/webs/#storage-entities","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import { IStorageEntity } from \"@pnp/sp/webs\"; // needs to be unique, GUIDs are great const key = \"my-storage-key\"; // read an existing entity const entity: IStorageEntity = await web.getStorageEntity(key); // setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site // you can get the tenant app catalog using the getTenantAppCatalogWeb const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb(); tenantAppCatalogWeb.setStorageEntity(key, \"new value\"); // set other properties tenantAppCatalogWeb.setStorageEntity(key, \"another value\", \"description\", \"comments\"); const entity2: IStorageEntity = await web.getStorageEntity(key); /* entity2 === { Value: \"another value\", Comment: \"comments\"; Description: \"description\", }; */ // you can also remove a storage entity await tenantAppCatalogWeb.removeStorageEntity(key);","title":"storage entities"},{"location":"sp/webs/#appcatalog-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/appcatalog\"; Selective 2 import \"@pnp/sp/appcatalog/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"appcatalog imports"},{"location":"sp/webs/#getappcatalog","text":"Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url. import { IApp } from \"@pnp/sp/appcatalog\"; const appWeb = web.getAppCatalog(); // appWeb url === web url const app: IApp = appWeb.getAppById(\"{your app id}\"); const appWeb2 = web.getAppCatalog(\"https://tenant.sharepoing.com/sites/someappcatalog\"); // appWeb2 url === \"https://tenant.sharepoing.com/sites/someappcatalog\"","title":"getAppCatalog"},{"location":"sp/webs/#client-side-pages-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/client-side-pages\"; Selective 2 import \"@pnp/sp/client-side-pages/web\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // simplest add a page example const page = await sp.web.addClientsidePage(\"mypage1\"); // simplest load a page example const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");","title":"client-side-pages imports"},{"location":"sp/webs/#content-type-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"content-type imports"},{"location":"sp/webs/#contenttypes","text":"Allows access to the collection of content types in this web. const cts = await web.contentTypes(); // you can also select fields and use other odata operators const cts2 = await web.contentTypes.select(\"Name\")();","title":"contentTypes"},{"location":"sp/webs/#features-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/features\"; Selective 2 import \"@pnp/sp/features/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"features imports"},{"location":"sp/webs/#features","text":"Allows access to the collection of content types in this web. const features = await web.features();","title":"features"},{"location":"sp/webs/#fields-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"fields imports"},{"location":"sp/webs/#fields","text":"Allows access to the collection of fields in this web. const fields = await web.fields();","title":"fields"},{"location":"sp/webs/#files-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/files\"; Selective 2 import \"@pnp/sp/files/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"files imports"},{"location":"sp/webs/#getfilebyserverrelativeurl","text":"Gets a file by server relative url import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativeUrl(\"/sites/dev/library/myfile.docx\");","title":"getFileByServerRelativeUrl"},{"location":"sp/webs/#getfilebyserverrelativepath","text":"Gets a file by server relative url if your file name contains # and % characters import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativePath(\"/sites/dev/library/my # file%.docx\");","title":"getFileByServerRelativePath"},{"location":"sp/webs/#folders-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"folders imports"},{"location":"sp/webs/#folders","text":"Gets the collection of folders in this web const folders = await web.folders(); // you can also filter and select as with any collection const folders2 = await web.folders.select(\"ServerRelativeUrl\", \"TimeLastModified\").filter(\"ItemCount gt 0\")(); // or get the most recently modified folder const folders2 = await web.folders.orderBy(\"TimeLastModified\").top(1)();","title":"folders"},{"location":"sp/webs/#rootfolder","text":"Gets the root folder of the web const folder = await web.rootFolder();","title":"rootFolder"},{"location":"sp/webs/#getfolderbyserverrelativeurl","text":"Gets a folder by server relative url import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativeUrl(\"/sites/dev/library\");","title":"getFolderByServerRelativeUrl"},{"location":"sp/webs/#getfolderbyserverrelativepath","text":"Gets a folder by server relative url if your folder name contains # and % characters import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativePath(\"/sites/dev/library/my # folder%/\");","title":"getFolderByServerRelativePath"},{"location":"sp/webs/#hubsites-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/hubsites\"; Selective 2 import \"@pnp/sp/hubsites/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"hubsites imports"},{"location":"sp/webs/#hubsitedata","text":"Gets hub site data for the current web import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; // get the data and force a refresh const data: IHubSiteWebData = await web.hubSiteData(true);","title":"hubSiteData"},{"location":"sp/webs/#synchubsitetheme","text":"Applies theme updates from the parent hub site collection await web.syncHubSiteTheme();","title":"syncHubSiteTheme"},{"location":"sp/webs/#lists-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/lists\"; Selective 2 import \"@pnp/sp/lists/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\";","title":"lists imports"},{"location":"sp/webs/#lists","text":"Gets the collection of all lists that are contained in the Web site import { ILists } from \"@pnp/sp/lists\"; const lists: ILists = web.lists; // you can always order the lists and select properties const data = await lists.select(\"Title\").orderBy(\"Title\")(); // and use other odata operators as well const data2 = await web.lists.top(3).orderBy(\"LastItemModifiedDate\")();","title":"lists"},{"location":"sp/webs/#siteuserinfolist","text":"Gets the UserInfo list of the site collection that contains the Web site import { IList } from \"@pnp/sp/lists\"; const list: IList = web.siteUserInfoList; const data = await list(); // or chain off that list to get additional details const items = await list.items.top(2)();","title":"siteUserInfoList"},{"location":"sp/webs/#defaultdocumentlibrary","text":"Get a reference the default documents library of a web import { IList } from \"@pnp/sp/lists\"; const list: IList = web.defaultDocumentLibrary;","title":"defaultDocumentLibrary"},{"location":"sp/webs/#customlisttemplates","text":"Gets the collection of all list definitions and list templates that are available import { IList } from \"@pnp/sp/lists\"; const templates = await web.customListTemplates(); // odata operators chain off the collection as expected const templates2 = await web.customListTemplates.select(\"Title\")();","title":"customListTemplates"},{"location":"sp/webs/#getlist","text":"Gets a list by server relative url (list's root folder) import { IList } from \"@pnp/sp/lists\"; const list: IList = web.getList(\"/sites/dev/lists/test\"); const listData = list();","title":"getList"},{"location":"sp/webs/#getcatalog","text":"Returns the list gallery on the site Name Value WebTemplateCatalog 111 WebPartCatalog 113 ListTemplateCatalog 114 MasterPageCatalog 116 SolutionCatalog 121 ThemeCatalog 123 DesignCatalog 124 AppDataCatalog 125 import { IList } from \"@pnp/sp/lists\"; const templateCatalog: IList = await web.getCatalog(111); const themeCatalog: IList = await web.getCatalog(123);","title":"getCatalog"},{"location":"sp/webs/#navigation-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/navigation\"; Selective 2 import \"@pnp/sp/navigation/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"navigation imports"},{"location":"sp/webs/#navigation","text":"Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar import { INavigation } from \"@pnp/sp/navigation\"; const nav: INavigation = web.navigation; const navData = await nav();","title":"navigation"},{"location":"sp/webs/#regional-settings-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/regional-settings\"; Selective 2 import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRegionalSettings } from \"@pnp/sp/navigation\"; const settings: IRegionalSettings = web.regionalSettings; const settingsData = await settings();","title":"regional-settings imports"},{"location":"sp/webs/#related-items-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/related-items\"; Selective 2 import \"@pnp/sp/related-items/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRelatedItemManager, IRelatedItem } from \"@pnp/sp/related-items\"; const manager: IRelatedItemManager = web.relatedItems; const data: IRelatedItem[] = await manager.getRelatedItems(\"{list name}\", 4);","title":"related-items imports"},{"location":"sp/webs/#security-imports","text":"Please see information around the available security methods in the security article .","title":"security imports"},{"location":"sp/webs/#sharing-imports","text":"Please see information around the available sharing methods in the sharing article .","title":"sharing imports"},{"location":"sp/webs/#site-groups-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/site-groups\"; Selective 2 import \"@pnp/sp/site-groups/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"site-groups imports"},{"location":"sp/webs/#sitegroups","text":"The site groups const groups = await web.siteGroups(); const groups2 = await web.siteGroups.top(2)();","title":"siteGroups"},{"location":"sp/webs/#associatedownergroup","text":"The web's owner group const group = await web.associatedOwnerGroup(); const users = await web.associatedOwnerGroup.users();","title":"associatedOwnerGroup"},{"location":"sp/webs/#associatedmembergroup","text":"The web's member group const group = await web.associatedMemberGroup(); const users = await web.associatedMemberGroup.users();","title":"associatedMemberGroup"},{"location":"sp/webs/#associatedvisitorgroup","text":"The web's visitor group const group = await web.associatedVisitorGroup(); const users = await web.associatedVisitorGroup.users();","title":"associatedVisitorGroup"},{"location":"sp/webs/#createdefaultassociatedgroups","text":"Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\"); // copy the role assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", true); // don't clear sub assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, false); // specify secondary owner, don't copy permissions, clear sub scopes await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, true, \"{second owner login}\");","title":"createDefaultAssociatedGroups"},{"location":"sp/webs/#site-users-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/site-users\"; Selective 2 import \"@pnp/sp/site-users/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"site-users imports"},{"location":"sp/webs/#siteusers","text":"The site users const users = await web.siteUsers(); const users2 = await web.siteUsers.top(5)(); const users3 = await web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent(\"i:0#.f|m\")}')`)();","title":"siteUsers"},{"location":"sp/webs/#currentuser","text":"Information on the current user const user = await web.currentUser(); // check the login name of the current user const user2 = await web.currentUser.select(\"LoginName\")();","title":"currentUser"},{"location":"sp/webs/#ensureuser","text":"Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web import { IWebEnsureUserResult } from \"@pnp/sp/site-users/\"; const result: IWebEnsureUserResult = await web.ensureUser(\"i:0#.f|membership|user@domain.onmicrosoft.com\");","title":"ensureUser"},{"location":"sp/webs/#getuserbyid","text":"Returns the user corresponding to the specified member identifier for the current web import { ISiteUser } from \"@pnp/sp/site-users/\"; const user: ISiteUser = web.getUserById(23); const userData = await user(); const userData2 = await user.select(\"LoginName\")();","title":"getUserById"},{"location":"sp/webs/#user-custom-actions-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"user-custom-actions imports"},{"location":"sp/webs/#usercustomactions","text":"Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection import { IUserCustomActions } from \"@pnp/sp/user-custom-actions\"; const actions: IUserCustomActions = web.userCustomActions; const actionsData = await actions();","title":"userCustomActions"},{"location":"sp/webs/#iwebinfosdata","text":"Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations. interface IWebInfosData { Configuration: number; Created: string; Description: string; Id: string; Language: number; LastItemModifiedDate: string; LastItemUserModifiedDate: string; ServerRelativeUrl: string; Title: string; WebTemplate: string; WebTemplateId: number; }","title":"IWebInfosData"},{"location":"sp-addinhelpers/","text":"@pnp/sp-addinhelpers \u00b6 This module contains classes to allow use of the libraries within a SharePoint add-in. Getting Started \u00b6 Install the library and all dependencies, npm install @pnp/sp @pnp/sp-addinhelpers --save Now you can make requests to the host web from your add-in using the crossDomainWeb method. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4)); Library Topics \u00b6 SPRequestExecutorClient SPRestAddIn","title":"sp-addinhelpers"},{"location":"sp-addinhelpers/#pnpsp-addinhelpers","text":"This module contains classes to allow use of the libraries within a SharePoint add-in.","title":"@pnp/sp-addinhelpers"},{"location":"sp-addinhelpers/#getting-started","text":"Install the library and all dependencies, npm install @pnp/sp @pnp/sp-addinhelpers --save Now you can make requests to the host web from your add-in using the crossDomainWeb method. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4));","title":"Getting Started"},{"location":"sp-addinhelpers/#library-topics","text":"SPRequestExecutorClient SPRestAddIn","title":"Library Topics"},{"location":"sp-addinhelpers/sp-request-executor-client/","text":"@pnp/sp-addinhelpers/sprequestexecutorclient \u00b6 The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request. Setup \u00b6 To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor sp.crossDomainWeb(addInWenUrl, hostWebUrl)().then(w => { console.log(JSON.stringify(w, null, 4)); });","title":"SPRequestExecutorClient"},{"location":"sp-addinhelpers/sp-request-executor-client/#pnpsp-addinhelperssprequestexecutorclient","text":"The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request.","title":"@pnp/sp-addinhelpers/sprequestexecutorclient"},{"location":"sp-addinhelpers/sp-request-executor-client/#setup","text":"To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor sp.crossDomainWeb(addInWenUrl, hostWebUrl)().then(w => { console.log(JSON.stringify(w, null, 4)); });","title":"Setup"},{"location":"sp-addinhelpers/sp-rest-addin/","text":"@pnp/sp-addinhelpers/sprestaddin \u00b6 This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4));","title":"SPRestAddIn"},{"location":"sp-addinhelpers/sp-rest-addin/#pnpsp-addinhelperssprestaddin","text":"This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4));","title":"@pnp/sp-addinhelpers/sprestaddin"},{"location":"v2/","text":"PnPjs is a collection of fluent libraries for consuming SharePoint, Graph, and Office 365 REST APIs in a type-safe way. You can use it within SharePoint Framework, Nodejs, or any JavaScript project. This an open source initiative and we encourage contributions and constructive feedback from the community. Animation of the library in use, note intellisense help in building your queries General Guidance \u00b6 These articles provide general guidance for working with the libraries. If you are migrating from v1 please review the transition guide . Getting Started Authentication Get Started Contributing npm scripts Polyfills Packages \u00b6 Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins Authentication \u00b6 We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out! Issues, Questions, Ideas \u00b6 Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project. Changelog \u00b6 Please review the CHANGELOG for release details on all library changes. Code of Conduct \u00b6 This project has adopted the Microsoft Open Source Code of Conduct . For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments. \"Sharing is Caring\" \u00b6 Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program . Disclaimer \u00b6 THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.","title":"Index"},{"location":"v2/#general-guidance","text":"These articles provide general guidance for working with the libraries. If you are migrating from v1 please review the transition guide . Getting Started Authentication Get Started Contributing npm scripts Polyfills","title":"General Guidance"},{"location":"v2/#packages","text":"Patterns and Practices client side libraries (PnPjs) are comprised of the packages listed below. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins","title":"Packages"},{"location":"v2/#authentication","text":"We have a new section dedicated to helping you figure out the best way to handle authentication in your application, check it out!","title":"Authentication"},{"location":"v2/#issues-questions-ideas","text":"Please log an issue using our template as a guide. This will let us track your request and ensure we respond. We appreciate any constructive feedback, questions, ideas, or bug reports with our thanks for giving back to the project.","title":"Issues, Questions, Ideas"},{"location":"v2/#changelog","text":"Please review the CHANGELOG for release details on all library changes.","title":"Changelog"},{"location":"v2/#code-of-conduct","text":"This project has adopted the Microsoft Open Source Code of Conduct . For more information see the Code of Conduct FAQ or contact opencode@microsoft.com with any additional questions or comments.","title":"Code of Conduct"},{"location":"v2/#sharing-is-caring","text":"Please use http://aka.ms/sppnp for the latest updates around the whole SharePoint Patterns and Practices (PnP) program .","title":"\"Sharing is Caring\""},{"location":"v2/#disclaimer","text":"THIS CODE IS PROVIDED AS IS WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.","title":"Disclaimer"},{"location":"v2/SPFx-on-premises/","text":"Workaround for on-premises SPFx TypeScript Version (SharePoint 2016 or 2019) \u00b6 Note this article applies to version 1.4.1 SharePoint Framework projects targeting on-premises only. When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premises it installs TypeScript version 2.2.2 (SP2016) or 2.4.2/2.4.1 (SP2019). Unfortunately this library relies on 3.6.4 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. npm i npm i -g rimraf # used to remove the node_modules folder (much better/faster) Ensure that the @pnp/sp package is already installed npm i @pnp/sp Remove the package-lock.json file & node_modules rimraf node_modules folder and execute npm install Open package-lock.json from the root folder Search for \"typescript\" or similar with version 2.4.1 (SP2019) 2.2.2 (SP2016) Replace \"2.4.1\" or \"2.2.2\" with \"3.6.4\" Search for the next \"typescript\" occurrence and replace the block with: JSON \"typescript\": { \"version\": \"3.6.4\", \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz\", \"integrity\": \"sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==\", \"dev\": true } Remove node_modules folder rimraf node_modules Run npm install Alternative using npm-force-resolutions \u00b6 Install resolutions package and TypeScript providing considered version explicitly: bash npm i -D npm-force-resolutions typescript@3.6.4 Add a resolution for TypeScript and preinstall script into package.json to a corresponding code blocks: JSON { \"scripts\": { \"preinstall\": \"npx npm-force-resolutions\" }, \"resolutions\": { \"typescript\": \"3.6.4\" } } Run npm install to trigger preinstall script and bumping TypeScript version into package-lock.json Run npm run build , should produce no errors Installing additional dependencies should be safe then.","title":"Workaround for on-premises SPFx TypeScript Version (SharePoint 2016 or 2019)"},{"location":"v2/SPFx-on-premises/#workaround-for-on-premises-spfx-typescript-version-sharepoint-2016-or-2019","text":"Note this article applies to version 1.4.1 SharePoint Framework projects targeting on-premises only. When using the Yeoman generator to create a SharePoint Framework 1.4.1 project targeting on-premises it installs TypeScript version 2.2.2 (SP2016) or 2.4.2/2.4.1 (SP2019). Unfortunately this library relies on 3.6.4 or later due to extensive use of default values for generic type parameters in the libraries. To work around this limitation you can follow the steps in this article. npm i npm i -g rimraf # used to remove the node_modules folder (much better/faster) Ensure that the @pnp/sp package is already installed npm i @pnp/sp Remove the package-lock.json file & node_modules rimraf node_modules folder and execute npm install Open package-lock.json from the root folder Search for \"typescript\" or similar with version 2.4.1 (SP2019) 2.2.2 (SP2016) Replace \"2.4.1\" or \"2.2.2\" with \"3.6.4\" Search for the next \"typescript\" occurrence and replace the block with: JSON \"typescript\": { \"version\": \"3.6.4\", \"resolved\": \"https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz\", \"integrity\": \"sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==\", \"dev\": true } Remove node_modules folder rimraf node_modules Run npm install","title":"Workaround for on-premises SPFx TypeScript Version (SharePoint 2016 or 2019)"},{"location":"v2/SPFx-on-premises/#alternative-using-npm-force-resolutions","text":"Install resolutions package and TypeScript providing considered version explicitly: bash npm i -D npm-force-resolutions typescript@3.6.4 Add a resolution for TypeScript and preinstall script into package.json to a corresponding code blocks: JSON { \"scripts\": { \"preinstall\": \"npx npm-force-resolutions\" }, \"resolutions\": { \"typescript\": \"3.6.4\" } } Run npm install to trigger preinstall script and bumping TypeScript version into package-lock.json Run npm run build , should produce no errors Installing additional dependencies should be safe then.","title":"Alternative using npm-force-resolutions"},{"location":"v2/getting-started/","text":"Getting Started \u00b6 These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. If you need to support older browsers please review the article on polyfills for required functionality. Install \u00b6 First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project. npm install @pnp/sp @pnp/graph --save Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples. import { getRandomString } from \"@pnp/core\"; (function() { // get and log a random string console.log(getRandomString(20)); })() Getting Started with SharePoint Framework \u00b6 The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises please read this note on a workaround for the included TypeScript version. If you are targeting SharePoint online you do not need to take any additional steps. Establish Context \u00b6 Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the SPFx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. The setup is always done in the onInit method to ensure it runs before your other life-cycle code. You can also set any other settings at this time. Using @pnp/core setup \u00b6 import { setup as pnpSetup } from \"@pnp/core\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present pnpSetup({ spfxContext: this.context }); }); } // ... Using @pnp/sp setup \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... Sp setup also supports passing just the SPFx context object directly as this is the most common case import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ... Using @pnp/graph setup \u00b6 import { graph } from \"@pnp/graph/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ... Establish context within an SPFx service \u00b6 Because you do not have full access to the context object within a service you need to setup things a little differently. If you do not need AAD tokens you can leave that part out and specify just the pageContext. import { ServiceKey, ServiceScope } from \"@microsoft/sp-core-library\"; import { PageContext } from \"@microsoft/sp-page-context\"; import { AadTokenProviderFactory } from \"@microsoft/sp-http\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; export interface ISampleService { getLists(): Promise; } export class SampleService { public static readonly serviceKey: ServiceKey = ServiceKey.create('SPFx:SampleService', SampleService); constructor(serviceScope: ServiceScope) { serviceScope.whenFinished(() => { const pageContext = serviceScope.consume(PageContext.serviceKey); const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey); // we need to \"spoof\" the context object with the parts we need for PnPjs sp.setup({ spfxContext: { aadTokenProviderFactory: tokenProviderFactory, pageContext: pageContext, } }); // This approach also works if you do not require AAD tokens // you don't need to do both // sp.setup({ // sp : { // baseUrl : pageContext.web.absoluteUrl // } // }); }); } public getLists(): Promise { return sp.web.lists(); } } Connect to SharePoint from Node \u00b6 Please see the main article on how we support node versions that require commonjs modules. npm i @pnp/sp-commonjs @pnp/nodejs-commonjs This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: import { sp } from \"@pnp/sp-commonjs\"; import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. // configure your node options (only once in your application) sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // make a call to SharePoint and log it in the console sp.web.select(\"Title\", \"Description\")().then(w => { console.log(JSON.stringify(w, null, 4)); }); Connect to Microsoft Graph From Node \u00b6 Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example. npm i @pnp/graph-commonjs @pnp/nodejs-commonjs Now we need to import what we'll need to call graph import { graph } from \"@pnp/graph-commonjs\"; import { AdalFetchClient } from \"@pnp/nodejs-commonjs\"; Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{mytenant}.onmicrosoft.com\", \"{application id}\", \"{application secret}\"); }, }, }); // make a call to Graph and get all the groups graph.groups().then(g => { console.log(JSON.stringify(g, null, 4)); }); Getting Started outside SharePoint Framework \u00b6 In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options. Set baseUrl through setup \u00b6 Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when working against unpatched versions of SharePoint 2013 as discussed here . This is optional for 2016 or SharePoint Online. The library does not support setting the headers to use nometadata as we rely on the metadata in the response to do some of the more complicated functions. Some of the pure data calls will probably work but it is not a supported configuration. import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { headers: { Accept: \"application/json;odata=verbose\", }, baseUrl: \"{Absolute SharePoint Web URL}\" }, }); const w = await sp.web(); Create Web instances directly \u00b6 Using this method you create the web directly with the url you want to use as the base. import { Web } from \"@pnp/sp/presets/all\"; const web = Web(\"{Absolute SharePoint Web URL}\"); const w = await web(); Next Steps \u00b6 Be sure to review the article describing all of the available settings across the libraries.","title":"Getting Started"},{"location":"v2/getting-started/#getting-started","text":"These libraries are geared towards folks working with TypeScript but will work equally well for JavaScript projects. To get started you need to install the libraries you need via npm. Many of the packages have a peer dependency to other packages with the @pnp namespace meaning you may need to install more than one package. All packages are released together eliminating version confusion - all packages will depend on packages with the same version number. If you need to support older browsers please review the article on polyfills for required functionality.","title":"Getting Started"},{"location":"v2/getting-started/#install","text":"First you will need to install those libraries you want to use in your application. Here we will install the most frequently used packages. This step applies to any environment or project. npm install @pnp/sp @pnp/graph --save Next we can import and use the functionality within our application. Below is a very simple example, please see the individual package documentation for more details and examples. import { getRandomString } from \"@pnp/core\"; (function() { // get and log a random string console.log(getRandomString(20)); })()","title":"Install"},{"location":"v2/getting-started/#getting-started-with-sharepoint-framework","text":"The @pnp/sp and @pnp/graph libraries are designed to work seamlessly within SharePoint Framework projects with a small amount of upfront configuration. If you are running in 2016 or 2019 on-premises please read this note on a workaround for the included TypeScript version. If you are targeting SharePoint online you do not need to take any additional steps.","title":"Getting Started with SharePoint Framework"},{"location":"v2/getting-started/#establish-context","text":"Because SharePoint Framework provides a local context to each component we need to set that context within the library. This allows us to determine request urls as well as use the SPFx HttpGraphClient within @pnp/graph. There are two ways to provide the SPFx context to the library. Either through the setup method imported from @pnp/core or using the setup method on either the @pnp/sp or @pnp/graph main export. All three are shown below and are equivalent, meaning if you are already importing the sp variable from @pnp/sp or the graph variable from @pnp/graph you should use their setup method to reduce imports. The setup is always done in the onInit method to ensure it runs before your other life-cycle code. You can also set any other settings at this time.","title":"Establish Context"},{"location":"v2/getting-started/#using-pnpcore-setup","text":"import { setup as pnpSetup } from \"@pnp/core\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present pnpSetup({ spfxContext: this.context }); }); } // ...","title":"Using @pnp/core setup"},{"location":"v2/getting-started/#using-pnpsp-setup","text":"import { sp } from \"@pnp/sp/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... Sp setup also supports passing just the SPFx context object directly as this is the most common case import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ...","title":"Using @pnp/sp setup"},{"location":"v2/getting-started/#using-pnpgraph-setup","text":"import { graph } from \"@pnp/graph/presets/all\"; // ... protected onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ...","title":"Using @pnp/graph setup"},{"location":"v2/getting-started/#establish-context-within-an-spfx-service","text":"Because you do not have full access to the context object within a service you need to setup things a little differently. If you do not need AAD tokens you can leave that part out and specify just the pageContext. import { ServiceKey, ServiceScope } from \"@microsoft/sp-core-library\"; import { PageContext } from \"@microsoft/sp-page-context\"; import { AadTokenProviderFactory } from \"@microsoft/sp-http\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; export interface ISampleService { getLists(): Promise; } export class SampleService { public static readonly serviceKey: ServiceKey = ServiceKey.create('SPFx:SampleService', SampleService); constructor(serviceScope: ServiceScope) { serviceScope.whenFinished(() => { const pageContext = serviceScope.consume(PageContext.serviceKey); const tokenProviderFactory = serviceScope.consume(AadTokenProviderFactory.serviceKey); // we need to \"spoof\" the context object with the parts we need for PnPjs sp.setup({ spfxContext: { aadTokenProviderFactory: tokenProviderFactory, pageContext: pageContext, } }); // This approach also works if you do not require AAD tokens // you don't need to do both // sp.setup({ // sp : { // baseUrl : pageContext.web.absoluteUrl // } // }); }); } public getLists(): Promise { return sp.web.lists(); } }","title":"Establish context within an SPFx service"},{"location":"v2/getting-started/#connect-to-sharepoint-from-node","text":"Please see the main article on how we support node versions that require commonjs modules. npm i @pnp/sp-commonjs @pnp/nodejs-commonjs This will install the logging, common, odata, sp, and nodejs packages. You can read more about what each package does starting on the packages page. Once these are installed you need to import them into your project, to communicate with SharePoint from node we'll need the following imports: import { sp } from \"@pnp/sp-commonjs\"; import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; Once you have imported the necessary resources you can update your code to setup the node fetch client as well as make a call to SharePoint. // configure your node options (only once in your application) sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // make a call to SharePoint and log it in the console sp.web.select(\"Title\", \"Description\")().then(w => { console.log(JSON.stringify(w, null, 4)); });","title":"Connect to SharePoint from Node"},{"location":"v2/getting-started/#connect-to-microsoft-graph-from-node","text":"Similar to the above you can also make calls to the Graph api from node using the libraries. Again we start with installing the required resources. You can see ./debug/launch/graph.ts for a live example. npm i @pnp/graph-commonjs @pnp/nodejs-commonjs Now we need to import what we'll need to call graph import { graph } from \"@pnp/graph-commonjs\"; import { AdalFetchClient } from \"@pnp/nodejs-commonjs\"; Now we can make our graph calls after setting up the Adal client. Note you'll need to setup an AzureAD App registration with the necessary permissions. graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{mytenant}.onmicrosoft.com\", \"{application id}\", \"{application secret}\"); }, }, }); // make a call to Graph and get all the groups graph.groups().then(g => { console.log(JSON.stringify(g, null, 4)); });","title":"Connect to Microsoft Graph From Node"},{"location":"v2/getting-started/#getting-started-outside-sharepoint-framework","text":"In some cases you may be working in a way such that we cannot determine the base url for the web. In this scenario you have two options.","title":"Getting Started outside SharePoint Framework"},{"location":"v2/getting-started/#set-baseurl-through-setup","text":"Here we are setting the baseUrl via the sp.setup method. We are also setting the headers to use verbose mode, something you may have to do when working against unpatched versions of SharePoint 2013 as discussed here . This is optional for 2016 or SharePoint Online. The library does not support setting the headers to use nometadata as we rely on the metadata in the response to do some of the more complicated functions. Some of the pure data calls will probably work but it is not a supported configuration. import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { headers: { Accept: \"application/json;odata=verbose\", }, baseUrl: \"{Absolute SharePoint Web URL}\" }, }); const w = await sp.web();","title":"Set baseUrl through setup"},{"location":"v2/getting-started/#create-web-instances-directly","text":"Using this method you create the web directly with the url you want to use as the base. import { Web } from \"@pnp/sp/presets/all\"; const web = Web(\"{Absolute SharePoint Web URL}\"); const w = await web();","title":"Create Web instances directly"},{"location":"v2/getting-started/#next-steps","text":"Be sure to review the article describing all of the available settings across the libraries.","title":"Next Steps"},{"location":"v2/nodejs-support/","text":"Working in Nodejs \u00b6 As outlined on the getting started page you can easily use the library with Nodejs, but there are some key differences you need to consider. But first a little history, you can skip this part if you just want to see how things work but we felt some folks might be interested. To make selective imports work we need to support es module syntax for client-side environments such as SPFx development. All versions of Nodejs that are currently LTS do not support es modules without flags (as of when this was written). We thought we had a scheme to handle this following the available guidance but ultimately it didn't work across all node versions and we unpublished 2.0.1. CommonJS Libraries \u00b6 Because of the difficulties of working with es modules in node we recommend using our mirror packages providing commonjs modules. These can be installed by using the package name and appending -commonjs, such as: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs These packages are built from the same source and released at the same time so all updates are included with each release. The only difference is that for the sp-commonjs and graph-commonjs packages we target the \"all\" preset as the entry point. This makes things a little easier in node where bundle sizes aren't an issue. You can see this in the nodejs-app sample . Here is that sample explained fully: Install Libraries \u00b6 We want to make a simple request to SharePoint so we need to first install the modules we need: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs --save We will also install TypeScript: npm install typescript --save-dev index.ts \u00b6 We will create an index.ts file and add the following code. You will need to update the site url, client id, and client secret to your values. This should be done using a settings file or something like Azure KeyVault for production, but for this example it is good enough. // our imports come from the -commonjs libs import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; // we call setup to use the node client sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{ site url }\", \"{ client id }\", \"{ client secret }\"); }, }, }); async function makeRequest() { // make a request to get the web's details const w = await sp.web(); console.log(JSON.stringify(w, null, 2)); } // get past no await at root of app makeRequest(); Don't forget you will need to register an app to get the client id and secret. Add a tsconfig.json \u00b6 Not strictly necessary but very useful to include a tsconfig.json to control how tsc transpiles your code to JavaScript { \"compilerOptions\": { \"module\": \"commonjs\", \"target\": \"esnext\", \"moduleResolution\": \"node\", \"declaration\": true, \"outDir\": \"dist\", \"skipLibCheck\": true, \"sourceMap\": true, \"lib\": [ \"dom\", \"esnext\" ] }, \"files\": [ \"./index.ts\" ] } Add an script to package.json \u00b6 We add the \"start\" script to the default package.json { \"name\": \"nodejs-app\", \"version\": \"1.0.0\", \"description\": \"Sample nodejs app using PnPjs\", \"main\": \"index.js\", \"scripts\": { \"start\": \"tsc -p . && node dist/index.js\", \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\" }, \"author\": \"\", \"license\": \"MIT\", \"dependencies\": { \"@pnp/nodejs-commonjs\": \"^2.0.2-5\", \"@pnp/sp-commonjs\": \"^2.0.2-5\" }, \"devDependencies\": { \"typescript\": \"^3.7.5\" } } Run It \u00b6 You can now run your program using: npm start","title":"Working in Nodejs"},{"location":"v2/nodejs-support/#working-in-nodejs","text":"As outlined on the getting started page you can easily use the library with Nodejs, but there are some key differences you need to consider. But first a little history, you can skip this part if you just want to see how things work but we felt some folks might be interested. To make selective imports work we need to support es module syntax for client-side environments such as SPFx development. All versions of Nodejs that are currently LTS do not support es modules without flags (as of when this was written). We thought we had a scheme to handle this following the available guidance but ultimately it didn't work across all node versions and we unpublished 2.0.1.","title":"Working in Nodejs"},{"location":"v2/nodejs-support/#commonjs-libraries","text":"Because of the difficulties of working with es modules in node we recommend using our mirror packages providing commonjs modules. These can be installed by using the package name and appending -commonjs, such as: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs These packages are built from the same source and released at the same time so all updates are included with each release. The only difference is that for the sp-commonjs and graph-commonjs packages we target the \"all\" preset as the entry point. This makes things a little easier in node where bundle sizes aren't an issue. You can see this in the nodejs-app sample . Here is that sample explained fully:","title":"CommonJS Libraries"},{"location":"v2/nodejs-support/#install-libraries","text":"We want to make a simple request to SharePoint so we need to first install the modules we need: npm install @pnp/sp-commonjs @pnp/nodejs-commonjs --save We will also install TypeScript: npm install typescript --save-dev","title":"Install Libraries"},{"location":"v2/nodejs-support/#indexts","text":"We will create an index.ts file and add the following code. You will need to update the site url, client id, and client secret to your values. This should be done using a settings file or something like Azure KeyVault for production, but for this example it is good enough. // our imports come from the -commonjs libs import { SPFetchClient } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; // we call setup to use the node client sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{ site url }\", \"{ client id }\", \"{ client secret }\"); }, }, }); async function makeRequest() { // make a request to get the web's details const w = await sp.web(); console.log(JSON.stringify(w, null, 2)); } // get past no await at root of app makeRequest(); Don't forget you will need to register an app to get the client id and secret.","title":"index.ts"},{"location":"v2/nodejs-support/#add-a-tsconfigjson","text":"Not strictly necessary but very useful to include a tsconfig.json to control how tsc transpiles your code to JavaScript { \"compilerOptions\": { \"module\": \"commonjs\", \"target\": \"esnext\", \"moduleResolution\": \"node\", \"declaration\": true, \"outDir\": \"dist\", \"skipLibCheck\": true, \"sourceMap\": true, \"lib\": [ \"dom\", \"esnext\" ] }, \"files\": [ \"./index.ts\" ] }","title":"Add a tsconfig.json"},{"location":"v2/nodejs-support/#add-an-script-to-packagejson","text":"We add the \"start\" script to the default package.json { \"name\": \"nodejs-app\", \"version\": \"1.0.0\", \"description\": \"Sample nodejs app using PnPjs\", \"main\": \"index.js\", \"scripts\": { \"start\": \"tsc -p . && node dist/index.js\", \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\" }, \"author\": \"\", \"license\": \"MIT\", \"dependencies\": { \"@pnp/nodejs-commonjs\": \"^2.0.2-5\", \"@pnp/sp-commonjs\": \"^2.0.2-5\" }, \"devDependencies\": { \"typescript\": \"^3.7.5\" } }","title":"Add an script to package.json"},{"location":"v2/nodejs-support/#run-it","text":"You can now run your program using: npm start","title":"Run It"},{"location":"v2/npm-scripts/","text":"Supported NPM Scripts \u00b6 As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies. This article outlines the current scripts we've implemented and how to use them, with available options and examples. Start \u00b6 Executes the serve command npm start Serve \u00b6 Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node. npm run serve Test \u00b6 Runs the tests and coverage for the library. Starting with 2.3.0 ONLY MSAL auth is supported for running the tests. More details on setting up MSAL for node. Options \u00b6 There are several options you can provide to the test command. All of these need to be separated using a \"--\" double hyphen so they are passed to the spawned sub-commands. Test a Single Package \u00b6 --package or -p This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory. # run only sp tests npm test -- -p sp # run only logging tests npm test -- -package logging Run a Single Test File \u00b6 --single or --s You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags. # run only sp web tests npm test -- -p sp -s web # run only graph groups tests npm test -- -package graph -single groups Specify a Site \u00b6 --site By default every time you run the tests a new sub-site is created below the site specified in your settings file . You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option. This option can be used with any or none of the other testing options. # run only sp web tests with a certain site npm test -- -p sp -s web --site https://some.site.com/sites/dev Cleanup \u00b6 --cleanup If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted. # clean up our testing site npm test -- --cleanup Logging \u00b6 --logging If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output. # enable logging during testing npm test -- --logging spVerbose \u00b6 Added in 2.0.13 --spverbose This flag will enable \"verbose\" OData mode for SharePoint tests. This flag is compatible with other flags. npm test -- --spverbose build \u00b6 Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed. npm run build package \u00b6 Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published. npm run package lint \u00b6 Runs the linter. npm run lint clean \u00b6 Removes any generated folders from the working directory. npm run clean","title":"Supported NPM Scripts"},{"location":"v2/npm-scripts/#supported-npm-scripts","text":"As you likely are aware you can embed scripts within package.json. Using this capability coupled with the knowledge that pretty much all of the tools we use now support code files (.js/.ts) as configuration we have removed gulp from our tooling and now execute our various actions via scripts. This is not a knock on gulp, it remains a great tool, rather an opportunity for us to remove some dependencies. This article outlines the current scripts we've implemented and how to use them, with available options and examples.","title":"Supported NPM Scripts"},{"location":"v2/npm-scripts/#start","text":"Executes the serve command npm start","title":"Start"},{"location":"v2/npm-scripts/#serve","text":"Starts a debugging server serving a bundled script with ./debug/serve/main.ts as the entry point. This allows you to run tests and debug code running within the context of a webpage rather than node. npm run serve","title":"Serve"},{"location":"v2/npm-scripts/#test","text":"Runs the tests and coverage for the library. Starting with 2.3.0 ONLY MSAL auth is supported for running the tests. More details on setting up MSAL for node.","title":"Test"},{"location":"v2/npm-scripts/#options","text":"There are several options you can provide to the test command. All of these need to be separated using a \"--\" double hyphen so they are passed to the spawned sub-commands.","title":"Options"},{"location":"v2/npm-scripts/#test-a-single-package","text":"--package or -p This option will only run the tests associated with the package you specify. The values are the folder names within the ./packages directory. # run only sp tests npm test -- -p sp # run only logging tests npm test -- -package logging","title":"Test a Single Package"},{"location":"v2/npm-scripts/#run-a-single-test-file","text":"--single or --s You can also run a specific file with a package. This option must be used with the single package option as you are essentially specifying the folder and file. This option uses either the flags. # run only sp web tests npm test -- -p sp -s web # run only graph groups tests npm test -- -package graph -single groups","title":"Run a Single Test File"},{"location":"v2/npm-scripts/#specify-a-site","text":"--site By default every time you run the tests a new sub-site is created below the site specified in your settings file . You can choose to reuse a site for testing, which saves time when re-running a set of tests frequently. Testing content is not deleted after tests, so if you need to inspect the created content from testing you may wish to forgo this option. This option can be used with any or none of the other testing options. # run only sp web tests with a certain site npm test -- -p sp -s web --site https://some.site.com/sites/dev","title":"Specify a Site"},{"location":"v2/npm-scripts/#cleanup","text":"--cleanup If you include this flag the testing web will be deleted once tests are complete. Useful for local testing where you do not need to inspect the web once the tests are complete. Works with any of the other options, be careful when specifying a web using --site as it will be deleted. # clean up our testing site npm test -- --cleanup","title":"Cleanup"},{"location":"v2/npm-scripts/#logging","text":"--logging If you include this flag a console logger will be subscribed and the log level will be set to Info. This will provide console output for all the requests being made during testing. This flag is compatible with all other flags - however unless you are trying to debug a specific test this will produce a lot of chatty output. # enable logging during testing npm test -- --logging","title":"Logging"},{"location":"v2/npm-scripts/#spverbose","text":"Added in 2.0.13 --spverbose This flag will enable \"verbose\" OData mode for SharePoint tests. This flag is compatible with other flags. npm test -- --spverbose","title":"spVerbose"},{"location":"v2/npm-scripts/#build","text":"Invokes the pnpbuild cli to transpile the TypeScript into JavaScript. All behavior is controlled via the tsconfig.json in the root of the project and sub folders as needed. npm run build","title":"build"},{"location":"v2/npm-scripts/#package","text":"Invokes the pnpbuild cli to create the package directories under the dist folder. This will allow you to see exactly what will end up in the npm packages once they are published. npm run package","title":"package"},{"location":"v2/npm-scripts/#lint","text":"Runs the linter. npm run lint","title":"lint"},{"location":"v2/npm-scripts/#clean","text":"Removes any generated folders from the working directory. npm run clean","title":"clean"},{"location":"v2/packages/","text":"Packages \u00b6 The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins","title":"Packages"},{"location":"v2/packages/#packages","text":"The following packages comprise the Patterns and Practices client side libraries. All of the packages are published as a set and depend on their peers within the @pnp scope. The latest published version is . @pnp/ adaljsclient Provides an adaljs wrapper suitable for use with PnPjs common Provides shared functionality across all pnp libraries config-store Provides a way to manage configuration within your application graph Provides a fluent api for working with Microsoft Graph logging Light-weight, subscribable logging framework msaljsclient Provides an msal wrapper suitable for use with PnPjs nodejs Provides functionality enabling the @pnp libraries within nodejs odata Provides shared odata functionality and base classes sp Provides a fluent api for working with SharePoint REST sp-addinhelpers Provides functionality for working within SharePoint add-ins","title":"Packages"},{"location":"v2/transition-guide/","text":"Transition Guide \u00b6 We have worked to make moving from @pnp library 1. to 2. as painless as possible, however there are some changes to how things work. The below guide we have provided an overview of what it takes to transition between the libraries. If we missed something, please let us know in the issues list so we can update the guide. Thanks! Installing @pnp libraries \u00b6 In version 1.* the libraries were setup as peer dependencies of each other requiring you to install each of them separately. We continue to believe this correctly describes the relationship, but recognize that basically nothing in the world accounts for peer dependencies. So we have updated the libraries to be dependencies. This makes it easier to install into your projects as you only need to install a single library: npm i --save @pnp/sp Selective Imports \u00b6 Another big change in v2 is the ability to selectively import the pieces you need from the libraries. This allows you to have smaller bundles and works well with tree-shaking. It does require you to have more import statements, which can potentially be a bit confusing at first. The selective imports apply to the sp and graph libraries. To help explain let's take the example of the Web object. In v1 Web includes a reference to pretty much everything else in the entire sp library. Meaning that if you use web (and you pretty much have to) you hold a ref to all the other pieces (like Fields, Lists, ContentTypes) even if you aren't using them. Because of that tree shaking can't do anything to reduce the bundle size because it \"thinks\" you are using them simply because they have been imported. To solve this in v2 the Web object no longer contains references to anything, it is a bare object with a few methods. If you look at the source you will see that, for example, there is no longer a \"lists\" property. These properties and methods are now added through selectively importing the functionality you need: Selectively Import Web lists functionality \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports the functionality for lists associated only with web import \"@pnp/sp/lists/web\"; const r = await sp.web.lists(); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports all the functionality for lists import \"@pnp/sp/lists\"; const r = await sp.web.lists(); Each of the docs pages shows the selective import paths for each sub-module (lists, items, etc.). Presets \u00b6 In addition to the ability to selectively import functionality you can import presets. This allows you to import an entire set of functionality in a single line. At launch the sp library will support two presets \"all\" and \"core\" with the graph library supporting \"all\". Using the \"all\" preset will match the functionality of v1. This can save you time in transitioning your projects so you can update to selective imports later. For new projects we recommend using the selective imports from day 1. To update your V1 projects to V2 you can replace all instances of \"@pnp/sp\" with \"@pnp/sp/presets/all\" and things should work as before (though some class names or other things may have changed, please review the change log and the rest of this guide). // V1 way of doing things: import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes, } from \"@pnp/sp\"; // V2 way with selective imports import { sp } from \"@pnp/sp\"; import { ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/clientside-pages\"; // V2 way with preset \"all\" import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/presets/all\"; Invokable Objects \u00b6 Another new feature is the addition of invokable objects. Previously where you used \"get()\" to invoke a request you can now leave it off. We have left the .get method in place so everyone's code wasn't broken immediately upon transitioning. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // old way (still works) const r1 = sp.web(); // invokable const r2 = sp.web(); The benefit is that objects can now support default actions that are not \"get\" but might be \"post\". And you save typing a few extra characters. This still work the same as with select or any of the other odata methods: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // invokable const r = sp.web.select(\"Title\", \"Url\")(); Factory Functions & Interfaces \u00b6 Another change in the library is in the structure of exports. We are no longer exporting the objects themselves, rather we are only exposing factory functions and interfaces. This allows us to decouple what developers use from our internal implementation. For folks using the fluent chain starting with sp you shouldn't need to update your code. If you are using any of the v1 classes directly you should just need to remove the \"new\" keyword and update the import path. The factory functions signature matches the constructor signature of the v1 objects. // v1 import { Web } from \"@pnp/sp\"; const web: Web = new Web(\"some absolute url\"); const r1 = web(); // v2 import { Web, IWeb } from \"@pnp/sp/webs\"; const web: IWeb = Web(\"some absolute url\"); const r2 = web(); Extension Methods \u00b6 Another new capability in v2 is the ability to extend objects and factories. This allows you to easily add methods or properties on a per-object basis. Please see the full article on extension methods describing this great new capability. CDN publishing \u00b6 Starting with v2 we will no longer create bundles for each of the packages. Historically these are not commonly used, don't work perfectly for everyone (there are a lot of ways to bundle things), and another piece we need to maintain. Instead we encourage folks to create their own bundles , optimized for their particular scenario. This will result in smaller overall bundle size and allow folks to bundle things to match their scenario. Please review the article on creating your custom bundles to see how to tailor bundles to your needs. The PnPjs bundle will remain, though it is designed only for backwards compatibility and we strongly recommend creating your own bundles, or directly importing the libraries into your projects using selective imports. Drop client-svc and sp-taxonomy libraries \u00b6 These libraries were created to allow folks to access and manage SharePoint taxonomy and manage metadata. Given that there is upcoming support for taxonomy via a supported REST API we will drop these two libraries. If working with taxonomy remains a core requirement of your application and we do not yet have support for the new apis, please remain on v1 for the time being. As of 2.0.6 we support reading the modern taxonomy API. Docs here","title":"Transition Guide"},{"location":"v2/transition-guide/#transition-guide","text":"We have worked to make moving from @pnp library 1. to 2. as painless as possible, however there are some changes to how things work. The below guide we have provided an overview of what it takes to transition between the libraries. If we missed something, please let us know in the issues list so we can update the guide. Thanks!","title":"Transition Guide"},{"location":"v2/transition-guide/#installing-pnp-libraries","text":"In version 1.* the libraries were setup as peer dependencies of each other requiring you to install each of them separately. We continue to believe this correctly describes the relationship, but recognize that basically nothing in the world accounts for peer dependencies. So we have updated the libraries to be dependencies. This makes it easier to install into your projects as you only need to install a single library: npm i --save @pnp/sp","title":"Installing @pnp libraries"},{"location":"v2/transition-guide/#selective-imports","text":"Another big change in v2 is the ability to selectively import the pieces you need from the libraries. This allows you to have smaller bundles and works well with tree-shaking. It does require you to have more import statements, which can potentially be a bit confusing at first. The selective imports apply to the sp and graph libraries. To help explain let's take the example of the Web object. In v1 Web includes a reference to pretty much everything else in the entire sp library. Meaning that if you use web (and you pretty much have to) you hold a ref to all the other pieces (like Fields, Lists, ContentTypes) even if you aren't using them. Because of that tree shaking can't do anything to reduce the bundle size because it \"thinks\" you are using them simply because they have been imported. To solve this in v2 the Web object no longer contains references to anything, it is a bare object with a few methods. If you look at the source you will see that, for example, there is no longer a \"lists\" property. These properties and methods are now added through selectively importing the functionality you need:","title":"Selective Imports"},{"location":"v2/transition-guide/#selectively-import-web-lists-functionality","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports the functionality for lists associated only with web import \"@pnp/sp/lists/web\"; const r = await sp.web.lists(); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // this imports all the functionality for lists import \"@pnp/sp/lists\"; const r = await sp.web.lists(); Each of the docs pages shows the selective import paths for each sub-module (lists, items, etc.).","title":"Selectively Import Web lists functionality"},{"location":"v2/transition-guide/#presets","text":"In addition to the ability to selectively import functionality you can import presets. This allows you to import an entire set of functionality in a single line. At launch the sp library will support two presets \"all\" and \"core\" with the graph library supporting \"all\". Using the \"all\" preset will match the functionality of v1. This can save you time in transitioning your projects so you can update to selective imports later. For new projects we recommend using the selective imports from day 1. To update your V1 projects to V2 you can replace all instances of \"@pnp/sp\" with \"@pnp/sp/presets/all\" and things should work as before (though some class names or other things may have changed, please review the change log and the rest of this guide). // V1 way of doing things: import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes, } from \"@pnp/sp\"; // V2 way with selective imports import { sp } from \"@pnp/sp\"; import { ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/clientside-pages\"; // V2 way with preset \"all\" import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes } from \"@pnp/sp/presets/all\";","title":"Presets"},{"location":"v2/transition-guide/#invokable-objects","text":"Another new feature is the addition of invokable objects. Previously where you used \"get()\" to invoke a request you can now leave it off. We have left the .get method in place so everyone's code wasn't broken immediately upon transitioning. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // old way (still works) const r1 = sp.web(); // invokable const r2 = sp.web(); The benefit is that objects can now support default actions that are not \"get\" but might be \"post\". And you save typing a few extra characters. This still work the same as with select or any of the other odata methods: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // invokable const r = sp.web.select(\"Title\", \"Url\")();","title":"Invokable Objects"},{"location":"v2/transition-guide/#factory-functions-interfaces","text":"Another change in the library is in the structure of exports. We are no longer exporting the objects themselves, rather we are only exposing factory functions and interfaces. This allows us to decouple what developers use from our internal implementation. For folks using the fluent chain starting with sp you shouldn't need to update your code. If you are using any of the v1 classes directly you should just need to remove the \"new\" keyword and update the import path. The factory functions signature matches the constructor signature of the v1 objects. // v1 import { Web } from \"@pnp/sp\"; const web: Web = new Web(\"some absolute url\"); const r1 = web(); // v2 import { Web, IWeb } from \"@pnp/sp/webs\"; const web: IWeb = Web(\"some absolute url\"); const r2 = web();","title":"Factory Functions & Interfaces"},{"location":"v2/transition-guide/#extension-methods","text":"Another new capability in v2 is the ability to extend objects and factories. This allows you to easily add methods or properties on a per-object basis. Please see the full article on extension methods describing this great new capability.","title":"Extension Methods"},{"location":"v2/transition-guide/#cdn-publishing","text":"Starting with v2 we will no longer create bundles for each of the packages. Historically these are not commonly used, don't work perfectly for everyone (there are a lot of ways to bundle things), and another piece we need to maintain. Instead we encourage folks to create their own bundles , optimized for their particular scenario. This will result in smaller overall bundle size and allow folks to bundle things to match their scenario. Please review the article on creating your custom bundles to see how to tailor bundles to your needs. The PnPjs bundle will remain, though it is designed only for backwards compatibility and we strongly recommend creating your own bundles, or directly importing the libraries into your projects using selective imports.","title":"CDN publishing"},{"location":"v2/transition-guide/#drop-client-svc-and-sp-taxonomy-libraries","text":"These libraries were created to allow folks to access and manage SharePoint taxonomy and manage metadata. Given that there is upcoming support for taxonomy via a supported REST API we will drop these two libraries. If working with taxonomy remains a core requirement of your application and we do not yet have support for the new apis, please remain on v1 for the time being. As of 2.0.6 we support reading the modern taxonomy API. Docs here","title":"Drop client-svc and sp-taxonomy libraries"},{"location":"v2/authentication/","text":"Authentication \u00b6 One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods. There are two places the PnPjs libraries can be used to connect to various services client (browser) or server . Utility Scenarios \u00b6 BearerTokenFetchClient LambdaFetchClient Client Scenarios \u00b6 SharePoint Framework Connect As: Current User User + AAD App via MSAL User + AAD App via ADAL Connect To: SharePoint as: Current User User + AAD App via MSAL Graph as: Current User User + AAD App via MSAL Both as: Current User User + AAD App via MSAL Single Page Application User + AAD App via MSAL Server Scenarios \u00b6 NodeJS SharePoint App Registration (App-Only) ADAL (App-Only) MSAL (App-Only) - coming soon","title":"Authentication"},{"location":"v2/authentication/#authentication","text":"One of the more challenging aspects of web development is ensuring you are properly authenticated to access the resources you need. This section is designed to guide you through connecting to the resources you need using the appropriate methods. There are two places the PnPjs libraries can be used to connect to various services client (browser) or server .","title":"Authentication"},{"location":"v2/authentication/#utility-scenarios","text":"BearerTokenFetchClient LambdaFetchClient","title":"Utility Scenarios"},{"location":"v2/authentication/#client-scenarios","text":"SharePoint Framework Connect As: Current User User + AAD App via MSAL User + AAD App via ADAL Connect To: SharePoint as: Current User User + AAD App via MSAL Graph as: Current User User + AAD App via MSAL Both as: Current User User + AAD App via MSAL Single Page Application User + AAD App via MSAL","title":"Client Scenarios"},{"location":"v2/authentication/#server-scenarios","text":"NodeJS SharePoint App Registration (App-Only) ADAL (App-Only) MSAL (App-Only) - coming soon","title":"Server Scenarios"},{"location":"v2/authentication/adaljsclient/","text":"@pnp/core/adalclient \u00b6 This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions. Where possible it is recommended to use the MSAL client . Getting Started \u00b6 Install the library and required dependencies npm install @pnp/adaljsclient --save Setup and Use inside SharePoint Framework \u00b6 Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes . This method will only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined below using the constructor to specify the values for an AAD Application you have setup. Calling the graph api \u00b6 By providing the context in the onInit we can create the adal client from known information. import { graph } from \"@pnp/graph\"; import { getRandomString } from \"@pnp/core\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup(this.context); }); } public render(): void { // here we are creating a team with a random name, required Group ReadWrite All permissions const teamName = `ATeam.${getRandomString(4)}`; this.domElement.innerHTML = `Hello, I am creating a team named \"${teamName}\" for you...`; graph.teams.create(teamName, \"This is a description\").then(t => { this.domElement.innerHTML += \"done!\"; }).catch(e => { this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`; }); } Calling the SharePoint API \u00b6 This example shows how to use the ADALClient with the @pnp/sp library to call an API secured with AAD from within SharePoint Framework. import { SPFxAdalClient } from \"@pnp/core\"; import { sp } from \"@pnp/sp/presets/all\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context, sp: { fetchClientFactory: () => new SPFxAdalClient(this.context), }, }); }); } public render(): void { sp.web().then(t => { this.domElement.innerHTML = JSON.stringify(t); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); } Calling the any API \u00b6 You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. import { FetchOptions } from \"@pnp/core\"; import { AdalClient } from \"@pnp/adaljsclient\"; import { ODataDefaultParser } from \"@pnp/queryable\"; // ... public render(): void { // create an ADAL Client const client = AdalClient.fromSPFxContext(this.context); // setup the request options const opts: FetchOptions = { method: \"GET\", headers: { \"Accept\": \"application/json\", }, }; // execute the request client.fetch(\"https://{tenant}.sharepoint.com/_api/web\", opts).then(response => { // create a parser to convert the response into JSON. // You can create your own, at this point you have a fetch Response to work with const parser = new ODataDefaultParser(); parser.parse(response).then(json => { this.domElement.innerHTML = JSON.stringify(json); }); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); } Manually Configure \u00b6 This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD. Setup and Use with Microsoft Graph \u00b6 This sample uses a custom AzureAd app you have created and granted the appropriate permissions. import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph\"; // configure the graph client // parameters are: // client id - the id of the application you created in azure ad // tenant - can be id or URL (shown) // redirect url - absolute url of a page to which your application and Azure AD app allows replies graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"https://myapp/singlesignon.aspx\"); }, }, }); try { // call the graph API const groups = await graph.groups(); console.log(JSON.stringify(groups, null, 4)); } catch (e) { console.error(e); } Nodejs Applications \u00b6 We have a dedicated node client in @pnp/nodejs.","title":"@pnp/core/adalclient"},{"location":"v2/authentication/adaljsclient/#pnpcoreadalclient","text":"This module contains the AdalClient class which can be used to authenticate to any AzureAD secured resource. It is designed to work seamlessly with SharePoint Framework's permissions. Where possible it is recommended to use the MSAL client .","title":"@pnp/core/adalclient"},{"location":"v2/authentication/adaljsclient/#getting-started","text":"Install the library and required dependencies npm install @pnp/adaljsclient --save","title":"Getting Started"},{"location":"v2/authentication/adaljsclient/#setup-and-use-inside-sharepoint-framework","text":"Using the SharePoint Framework is the preferred way to make use of the AdalClient as we can use the AADTokenProvider to efficiently get tokens on your behalf. You can also read more about how this process works and the necessary SPFx configurations in the SharePoint Framework 1.6 release notes . This method will only work for SharePoint Framework >= 1.6. For earlier versions of SharePoint Framework you can still use the AdalClient as outlined below using the constructor to specify the values for an AAD Application you have setup.","title":"Setup and Use inside SharePoint Framework"},{"location":"v2/authentication/adaljsclient/#calling-the-graph-api","text":"By providing the context in the onInit we can create the adal client from known information. import { graph } from \"@pnp/graph\"; import { getRandomString } from \"@pnp/core\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup(this.context); }); } public render(): void { // here we are creating a team with a random name, required Group ReadWrite All permissions const teamName = `ATeam.${getRandomString(4)}`; this.domElement.innerHTML = `Hello, I am creating a team named \"${teamName}\" for you...`; graph.teams.create(teamName, \"This is a description\").then(t => { this.domElement.innerHTML += \"done!\"; }).catch(e => { this.domElement.innerHTML = `Oops, I ran into a problem...${JSON.stringify(e, null, 4)}`; }); }","title":"Calling the graph api"},{"location":"v2/authentication/adaljsclient/#calling-the-sharepoint-api","text":"This example shows how to use the ADALClient with the @pnp/sp library to call an API secured with AAD from within SharePoint Framework. import { SPFxAdalClient } from \"@pnp/core\"; import { sp } from \"@pnp/sp/presets/all\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context, sp: { fetchClientFactory: () => new SPFxAdalClient(this.context), }, }); }); } public render(): void { sp.web().then(t => { this.domElement.innerHTML = JSON.stringify(t); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); }","title":"Calling the SharePoint API"},{"location":"v2/authentication/adaljsclient/#calling-the-any-api","text":"You can also use the AdalClient to execute AAD authenticated requests to any API which is properly configured to accept the incoming tokens. This approach will only work within SharePoint Framework >= 1.6. Here we call the SharePoint REST API without the sp library as an example. import { FetchOptions } from \"@pnp/core\"; import { AdalClient } from \"@pnp/adaljsclient\"; import { ODataDefaultParser } from \"@pnp/queryable\"; // ... public render(): void { // create an ADAL Client const client = AdalClient.fromSPFxContext(this.context); // setup the request options const opts: FetchOptions = { method: \"GET\", headers: { \"Accept\": \"application/json\", }, }; // execute the request client.fetch(\"https://{tenant}.sharepoint.com/_api/web\", opts).then(response => { // create a parser to convert the response into JSON. // You can create your own, at this point you have a fetch Response to work with const parser = new ODataDefaultParser(); parser.parse(response).then(json => { this.domElement.innerHTML = JSON.stringify(json); }); }).catch(e => { this.domElement.innerHTML = JSON.stringify(e); }); }","title":"Calling the any API"},{"location":"v2/authentication/adaljsclient/#manually-configure","text":"This example shows setting up and using the AdalClient to make queries using information you have setup. You can review this article for more information on setting up and securing any application using AzureAD.","title":"Manually Configure"},{"location":"v2/authentication/adaljsclient/#setup-and-use-with-microsoft-graph","text":"This sample uses a custom AzureAd app you have created and granted the appropriate permissions. import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph\"; // configure the graph client // parameters are: // client id - the id of the application you created in azure ad // tenant - can be id or URL (shown) // redirect url - absolute url of a page to which your application and Azure AD app allows replies graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"https://myapp/singlesignon.aspx\"); }, }, }); try { // call the graph API const groups = await graph.groups(); console.log(JSON.stringify(groups, null, 4)); } catch (e) { console.error(e); }","title":"Setup and Use with Microsoft Graph"},{"location":"v2/authentication/adaljsclient/#nodejs-applications","text":"We have a dedicated node client in @pnp/nodejs.","title":"Nodejs Applications"},{"location":"v2/authentication/bearertokenclient/","text":"@pnp/core/BearerTokenFetchClient \u00b6 The BearerTokenFetchClient takes a single parameter representing an access token and uses it to make the requests. The disadvantage to this approach is not knowing to where the request will be sent, which in some cases is fine. An alternative is the LambdaFetchClient Static \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { BearerTokenFetchClient } from \"@pnp/core\"; import { myTokenFactory } from \"./my-auth.js\"; graph.setup({ graph: { fetchClientFactory: () => { // note this method is not async, so your logic here cannot await. // Please see the LambdaFetchClient if you have a need for async support. const token = myTokenFactory(); return new BearerTokenFetchClient(token); }, }, });","title":"@pnp/core/BearerTokenFetchClient"},{"location":"v2/authentication/bearertokenclient/#pnpcorebearertokenfetchclient","text":"The BearerTokenFetchClient takes a single parameter representing an access token and uses it to make the requests. The disadvantage to this approach is not knowing to where the request will be sent, which in some cases is fine. An alternative is the LambdaFetchClient","title":"@pnp/core/BearerTokenFetchClient"},{"location":"v2/authentication/bearertokenclient/#static","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { BearerTokenFetchClient } from \"@pnp/core\"; import { myTokenFactory } from \"./my-auth.js\"; graph.setup({ graph: { fetchClientFactory: () => { // note this method is not async, so your logic here cannot await. // Please see the LambdaFetchClient if you have a need for async support. const token = myTokenFactory(); return new BearerTokenFetchClient(token); }, }, });","title":"Static"},{"location":"v2/authentication/client-spa/","text":"Authentication in Single Page Application \u00b6 If you are writing a single page application deployed outside SharePoint it is recommended to use the MSAL client. You can find further details on the settings in the MSAL docs . You will need to ensure that you grant the permissions required to the application you are trying to use. import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); const data = await graph.me();","title":"Authentication in Single Page Application"},{"location":"v2/authentication/client-spa/#authentication-in-single-page-application","text":"If you are writing a single page application deployed outside SharePoint it is recommended to use the MSAL client. You can find further details on the settings in the MSAL docs . You will need to ensure that you grant the permissions required to the application you are trying to use. import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); const data = await graph.me();","title":"Authentication in Single Page Application"},{"location":"v2/authentication/client-spfx/","text":"Authentication in SharePoint Framework \u00b6 Auth as Current User \u00b6 PnPjs is designed to work as easily as possible within the SharePoint Framework so the authentication setup is very simple for the base case. Supply the current SharePoint Framework context to the library. This works for both SharePoint authentication and Graph authentication using the current user. Graph permissions are controlled by the permissions granted to the SharePoint shared application within your tenant. The below example is taken from a SharePoint Framework webpart. Connect to SharePoint as Current User \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ... Connect to Graph as Current User \u00b6 Permissions for this graph connection are controlled by the Shared SharePoint Application. You can target other applications using the MSAL Client . import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present // this will use the ADAL client behind the scenes with no additional configuration work graph.setup(this.context); } // ... MSAL Client \u00b6 You might want/need to use a client configured to use your own AAD application and not the shared SharePoint application. You can do so using the MSAL client . Here we show this using graph, this works the same with any of the setup strategies . Please see the MSAL library docs for more details on what values to supply in the configuration. Note: you must install the @pnp/msaljsclient client package before using it import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); } // ... ADAL Client \u00b6 You can use the ADAL client from within SPFx, though it is recommended to transition to the MSAL client. Note: you must install the @pnp/adaljsclient client package before using it import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"\"); }, }); } // ...","title":"Authentication in SharePoint Framework"},{"location":"v2/authentication/client-spfx/#authentication-in-sharepoint-framework","text":"","title":"Authentication in SharePoint Framework"},{"location":"v2/authentication/client-spfx/#auth-as-current-user","text":"PnPjs is designed to work as easily as possible within the SharePoint Framework so the authentication setup is very simple for the base case. Supply the current SharePoint Framework context to the library. This works for both SharePoint authentication and Graph authentication using the current user. Graph permissions are controlled by the permissions granted to the SharePoint shared application within your tenant. The below example is taken from a SharePoint Framework webpart.","title":"Auth as Current User"},{"location":"v2/authentication/client-spfx/#connect-to-sharepoint-as-current-user","text":"import { sp } from \"@pnp/sp/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present sp.setup(this.context); } // ...","title":"Connect to SharePoint as Current User"},{"location":"v2/authentication/client-spfx/#connect-to-graph-as-current-user","text":"Permissions for this graph connection are controlled by the Shared SharePoint Application. You can target other applications using the MSAL Client . import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present // this will use the ADAL client behind the scenes with no additional configuration work graph.setup(this.context); } // ...","title":"Connect to Graph as Current User"},{"location":"v2/authentication/client-spfx/#msal-client","text":"You might want/need to use a client configured to use your own AAD application and not the shared SharePoint application. You can do so using the MSAL client . Here we show this using graph, this works the same with any of the setup strategies . Please see the MSAL library docs for more details on what values to supply in the configuration. Note: you must install the @pnp/msaljsclient client package before using it import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/common\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"{your redirect uri}\", }, cache: { cacheLocation: \"sessionStorage\", }, }, [\"email\", \"Files.Read.All\", \"User.Read.All\"]), }, }); } // ...","title":"MSAL Client"},{"location":"v2/authentication/client-spfx/#adal-client","text":"You can use the ADAL client from within SPFx, though it is recommended to transition to the MSAL client. Note: you must install the @pnp/adaljsclient client package before using it import { AdalClient } from \"@pnp/adaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; // ... protected async onInit(): Promise { await super.onInit(); // other init code may be present graph.setup({ graph: { fetchClientFactory: () => { return new AdalClient( \"00000000-0000-0000-0000-000000000000\", \"{tenant}.onmicrosoft.com\", \"\"); }, }); } // ...","title":"ADAL Client"},{"location":"v2/authentication/lambdaclient/","text":"@pnp/core/LambdaFetchClient \u00b6 The LambdaFetchClient class allows you to provide an async function that returns an access token using any logic/supporting libraries you need. This provides total freedom to define how you do authentication, so long as it results in a usable Bearer token to call the target resource. The advantage to the LambdaFetchClient is that you get the url for each request, meaning your logic can account for where the request is headed. The token function should be as efficient as possible as it's logic must complete before each request will be sent. Signature \u00b6 The LambdaFetchClient accepts a single argument of type ILambdaTokenFactoryParams. // signature of method, the return string is the access token (parms: ILambdaTokenFactoryParams) => Promise // ILambdaTokenFactoryParams export interface ILambdaTokenFactoryParams { /** * Url to which the request for which we are requesting a token will be sent */ url: string; /** * Any options supplied for the request */ options: IFetchOptions; } @azure/msal-browser example \u00b6 This example shows how to use @azure/msal-browser along with LambdaFetchClient to achieve signin. msal-browser has many possible configurations which are described within their documentation. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { LambdaFetchClient } from \"@pnp/core\"; import { PublicClientApplication, Configuration } from \"@azure/msal-browser\"; const config: Configuration = { auth: { clientId: \"{client id}\", authority: \"https://login.microsoftonline.com/common/\" } } // create a single application, could also create this within the lambda client, but it would create a new applicaiton per request const msal = new PublicClientApplication(config); // create a new instance of the lambda fetch client const client = new LambdaFetchClient(async () => { const request = { scopes: [\"User.Read.All\"], }; const response = await msal.loginPopup(request); // lamba returns the access token return response.accessToken; }); // setup graph with the client graph.setup({ graph: { fetchClientFactory: () => client, }, }); // execute the request to graph which will use the client defined above const result = await graph.users();","title":"@pnp/core/LambdaFetchClient"},{"location":"v2/authentication/lambdaclient/#pnpcorelambdafetchclient","text":"The LambdaFetchClient class allows you to provide an async function that returns an access token using any logic/supporting libraries you need. This provides total freedom to define how you do authentication, so long as it results in a usable Bearer token to call the target resource. The advantage to the LambdaFetchClient is that you get the url for each request, meaning your logic can account for where the request is headed. The token function should be as efficient as possible as it's logic must complete before each request will be sent.","title":"@pnp/core/LambdaFetchClient"},{"location":"v2/authentication/lambdaclient/#signature","text":"The LambdaFetchClient accepts a single argument of type ILambdaTokenFactoryParams. // signature of method, the return string is the access token (parms: ILambdaTokenFactoryParams) => Promise // ILambdaTokenFactoryParams export interface ILambdaTokenFactoryParams { /** * Url to which the request for which we are requesting a token will be sent */ url: string; /** * Any options supplied for the request */ options: IFetchOptions; }","title":"Signature"},{"location":"v2/authentication/lambdaclient/#azuremsal-browser-example","text":"This example shows how to use @azure/msal-browser along with LambdaFetchClient to achieve signin. msal-browser has many possible configurations which are described within their documentation. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import { LambdaFetchClient } from \"@pnp/core\"; import { PublicClientApplication, Configuration } from \"@azure/msal-browser\"; const config: Configuration = { auth: { clientId: \"{client id}\", authority: \"https://login.microsoftonline.com/common/\" } } // create a single application, could also create this within the lambda client, but it would create a new applicaiton per request const msal = new PublicClientApplication(config); // create a new instance of the lambda fetch client const client = new LambdaFetchClient(async () => { const request = { scopes: [\"User.Read.All\"], }; const response = await msal.loginPopup(request); // lamba returns the access token return response.accessToken; }); // setup graph with the client graph.setup({ graph: { fetchClientFactory: () => client, }, }); // execute the request to graph which will use the client defined above const result = await graph.users();","title":"@azure/msal-browser example"},{"location":"v2/authentication/msaljsclient/","text":"msaljsclient - MSAL Client for PnPjs \u00b6 The MSAL client is a thin wrapper around the MSAL library adapting it for use with PnPjs's request pipeline. Install \u00b6 You need to install the MSAL client before using it. This is in addition to installing the other PnPjs libraries you require. npm install @pnp/msaljsclient --save Configure \u00b6 The PnP client is a very thin wrapper around the MSAL library and you can supply any of the arguments supported. These are described in the MSAL docs . The basic configuration values you need (at least from our testing) are client id, authority, and redirectUri. The other options are settable but not required. This article is not intended to be an exhaustive discussion of all the MSAL configuration possibilities, please see the official docs to understand all of the available options. The second parameter when configuring the PnP client is the list of scope you are seeking to use. These must be configured and properly granted within AAD and you can request one or more scopes as needed for the current scenario. Use in SPFx \u00b6 Calling SharePoint via MSAL \u00b6 When calling the SharePoint REST API we must use only a special scope \"https://{tenant}.sharepoint.com/.default\" import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/mytentant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://mytentant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"https://mytentant.sharepoint.com/.default\"]), }, }); const r = await sp.web(); Calling Graph via MSAL \u00b6 When calling the graph API you must specify the scopes you need and ensure they are configured in AAD import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups(); Use in Single Page Applications \u00b6 You can also use the PnPjs MSAL client within your SPA applications. Please review the various settings to ensure you are configuring MSAL as needed for your application import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://myapp.com/login.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups(); Get a Token \u00b6 You can also use the client to get a token if you need a token for use outside the PnPjs libraries import { MsalClient } from \"@pnp/msaljsclient\"; // note we do not provide scopes here as the second parameter. We certainly could and will get a token // based on those scopes by making a call to getToken() without a param. const client = new MsalClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant}.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://{tenant}.sharepoint.com/sites/dev/SitePages/webpacktest.aspx\", }, }); const token = await client.getToken([\"Group.Read.All\"]); const token2 = await client.getToken([\"Files.Read\"]);","title":"msaljsclient - MSAL Client for PnPjs"},{"location":"v2/authentication/msaljsclient/#msaljsclient-msal-client-for-pnpjs","text":"The MSAL client is a thin wrapper around the MSAL library adapting it for use with PnPjs's request pipeline.","title":"msaljsclient - MSAL Client for PnPjs"},{"location":"v2/authentication/msaljsclient/#install","text":"You need to install the MSAL client before using it. This is in addition to installing the other PnPjs libraries you require. npm install @pnp/msaljsclient --save","title":"Install"},{"location":"v2/authentication/msaljsclient/#configure","text":"The PnP client is a very thin wrapper around the MSAL library and you can supply any of the arguments supported. These are described in the MSAL docs . The basic configuration values you need (at least from our testing) are client id, authority, and redirectUri. The other options are settable but not required. This article is not intended to be an exhaustive discussion of all the MSAL configuration possibilities, please see the official docs to understand all of the available options. The second parameter when configuring the PnP client is the list of scope you are seeking to use. These must be configured and properly granted within AAD and you can request one or more scopes as needed for the current scenario.","title":"Configure"},{"location":"v2/authentication/msaljsclient/#use-in-spfx","text":"","title":"Use in SPFx"},{"location":"v2/authentication/msaljsclient/#calling-sharepoint-via-msal","text":"When calling the SharePoint REST API we must use only a special scope \"https://{tenant}.sharepoint.com/.default\" import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/mytentant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://mytentant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"https://mytentant.sharepoint.com/.default\"]), }, }); const r = await sp.web();","title":"Calling SharePoint via MSAL"},{"location":"v2/authentication/msaljsclient/#calling-graph-via-msal","text":"When calling the graph API you must specify the scopes you need and ensure they are configured in AAD import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups();","title":"Calling Graph via MSAL"},{"location":"v2/authentication/msaljsclient/#use-in-single-page-applications","text":"You can also use the PnPjs MSAL client within your SPA applications. Please review the various settings to ensure you are configuring MSAL as needed for your application import { MsalClientSetup } from \"@pnp/msaljsclient\"; import { graph } from \"@pnp/graph/presets/all\"; graph.setup({ graph: { fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://myapp.com/login.aspx\", }, }, [\"Group.Read.All\"]), }, }); const r = await graph.groups();","title":"Use in Single Page Applications"},{"location":"v2/authentication/msaljsclient/#get-a-token","text":"You can also use the client to get a token if you need a token for use outside the PnPjs libraries import { MsalClient } from \"@pnp/msaljsclient\"; // note we do not provide scopes here as the second parameter. We certainly could and will get a token // based on those scopes by making a call to getToken() without a param. const client = new MsalClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant}.onmicrosoft.com/\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://{tenant}.sharepoint.com/sites/dev/SitePages/webpacktest.aspx\", }, }); const token = await client.getToken([\"Group.Read.All\"]); const token2 = await client.getToken([\"Files.Read\"]);","title":"Get a Token"},{"location":"v2/authentication/server-nodejs/","text":"Authentication in Nodejs \u00b6 SharePoint App Registration \u00b6 Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Auth . Within the PnPjs testing framework we make use of SharePoint App Registration. This uses the SPFetchClient client from the nodejs package. This client works based on the legacy SharePoint App Registration model making use of a client and secret granted permissions through AppInv.aspx. This method works and at the time of writing has no published end date. See: details on how to register a legacy SharePoint application . import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web(); MSAL \u00b6 Added in 2.0.11 You can now use the @azure/msal-node client with PnPjs using MsalFetchClient. You must configure an AAD application with the appropriate permissions for your application. At the time this article was written the msal-node package is not yet GA. Call Graph \u00b6 You can call the Microsoft Graph API with a client id and secret or certificate (see SharePoint example for cert auth) import { graph } from \"@pnp/graph/presets/all\"; // configure your node options graph.setup({ graph: { fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientId: \"{guid}\", clientSecret: \"{client secret}\", } }); }, }, }); const userInfo = await graph.users(); Call SharePoint \u00b6 To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below. mkdir \\temp cd \\temp openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle' openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass Using the above code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration. You need to set the baseUrl property when using the MsalFetchClient import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const w = await sp.web(); ADAL \u00b6 The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. See: More details on the node client import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"Authentication in Nodejs"},{"location":"v2/authentication/server-nodejs/#authentication-in-nodejs","text":"","title":"Authentication in Nodejs"},{"location":"v2/authentication/server-nodejs/#sharepoint-app-registration","text":"Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Auth . Within the PnPjs testing framework we make use of SharePoint App Registration. This uses the SPFetchClient client from the nodejs package. This client works based on the legacy SharePoint App Registration model making use of a client and secret granted permissions through AppInv.aspx. This method works and at the time of writing has no published end date. See: details on how to register a legacy SharePoint application . import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web();","title":"SharePoint App Registration"},{"location":"v2/authentication/server-nodejs/#msal","text":"Added in 2.0.11 You can now use the @azure/msal-node client with PnPjs using MsalFetchClient. You must configure an AAD application with the appropriate permissions for your application. At the time this article was written the msal-node package is not yet GA.","title":"MSAL"},{"location":"v2/authentication/server-nodejs/#call-graph","text":"You can call the Microsoft Graph API with a client id and secret or certificate (see SharePoint example for cert auth) import { graph } from \"@pnp/graph/presets/all\"; // configure your node options graph.setup({ graph: { fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientId: \"{guid}\", clientSecret: \"{client secret}\", } }); }, }, }); const userInfo = await graph.users();","title":"Call Graph"},{"location":"v2/authentication/server-nodejs/#call-sharepoint","text":"To call the SharePoint APIs via MSAL you are required to use certificate authentication with your application. Fully covering certificates is outside the scope of these docs, but the following commands were used with openssl to create testing certs for the sample code below. mkdir \\temp cd \\temp openssl req -x509 -newkey rsa:2048 -keyout keytmp.pem -out cert.pem -days 365 -passout pass:HereIsMySuperPass -subj '/C=US/ST=Washington/L=Seattle' openssl rsa -in keytmp.pem -out key.pem -passin pass:HereIsMySuperPass Using the above code you end up with three files, \"cert.pem\", \"key.pem\", and \"keytmp.pem\". The \"cert.pem\" file is uploaded to your AAD application registration. The \"key.pem\" is read as the private key for the configuration. You need to set the baseUrl property when using the MsalFetchClient import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}/\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const w = await sp.web();","title":"Call SharePoint"},{"location":"v2/authentication/server-nodejs/#adal","text":"The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. See: More details on the node client import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"ADAL"},{"location":"v2/authentication/sp-app-registration/","text":"Legacy SharePoint App Registration \u00b6 This section outlines how to register for a client id and secret for use in the above code. Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Authentication . Register An Add-In \u00b6 Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article. Grant Your Add-In Permissions \u00b6 Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the App Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note that the above XML will grant full tenant control. This is OK for testing, but you should grant only those permissions necessary for your application in production.","title":"Legacy SharePoint App Registration"},{"location":"v2/authentication/sp-app-registration/#legacy-sharepoint-app-registration","text":"This section outlines how to register for a client id and secret for use in the above code. Due to a recent change in how SPO is configured NEW tenants will have ACS authentication disabled by default. You can read more details in this article . For testing we recommend using MSAL Certificate Authentication .","title":"Legacy SharePoint App Registration"},{"location":"v2/authentication/sp-app-registration/#register-an-add-in","text":"Before you can begin running tests you need to register a low-trust add-in with SharePoint. This is primarily designed for Office 365, but can work on-premises if you configure your farm accordingly . Navigation to {site url}/_layouts/appregnew.aspx Click \"Generate\" for both the Client Id and Secret values Give you add-in a title, this can be anything but will let you locate it in the list of add-in permissions Provide a fake value for app domain and redirect uri Click \"Create\" Copy the returned block of text containing the client id and secret as well as app name for your records and later in this article.","title":"Register An Add-In"},{"location":"v2/authentication/sp-app-registration/#grant-your-add-in-permissions","text":"Now that we have created an add-in registration we need to tell SharePoint what permissions it can use. Due to an update in SharePoint Online you now have to register add-ins with certain permissions in the admin site . Navigate to {admin site url}/_layouts/appinv.aspx Paste your client id from the above section into the App Id box and click \"Lookup\" You should see the information populated into the form from the last section, if not ensure you have the correct id value Paste the below XML into the permissions request xml box and hit \"Create\" You should get a confirmation message. Note that the above XML will grant full tenant control. This is OK for testing, but you should grant only those permissions necessary for your application in production.","title":"Grant Your Add-In Permissions"},{"location":"v2/common/","text":"@pnp/core \u00b6 The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/core --save Import and use functionality, see details on modules below. import { getGUID } from \"@pnp/core\"; console.log(getGUID()); Exports \u00b6 collections libconfig netutil storage util Custom HttpClient","title":"@pnp/core"},{"location":"v2/common/#pnpcore","text":"The common modules provides a set of utilities classes and reusable building blocks used throughout the @pnp modules. They can be used within your applications as well.","title":"@pnp/core"},{"location":"v2/common/#getting-started","text":"Install the library and required dependencies npm install @pnp/core --save Import and use functionality, see details on modules below. import { getGUID } from \"@pnp/core\"; console.log(getGUID());","title":"Getting Started"},{"location":"v2/common/#exports","text":"collections libconfig netutil storage util Custom HttpClient","title":"Exports"},{"location":"v2/common/collections/","text":"@pnp/core/collections \u00b6 The collections module provides typings and classes related to working with dictionaries. TypedHash \u00b6 Interface used to described an object with string keys corresponding to values of type T export interface TypedHash { [key: string]: T; } objectToMap \u00b6 Converts a plain object to a Map instance const map = objectToMap({ a: \"b\", c: \"d\"}); mergeMaps \u00b6 Merges two or more maps, overwriting values with the same key. Last value in wins. const m1 = new Map(); const m2 = new Map(); const m3 = new Map(); const m4 = new Map(); const m = mergeMaps(m1, m2, m3, m4);","title":"@pnp/core/collections"},{"location":"v2/common/collections/#pnpcorecollections","text":"The collections module provides typings and classes related to working with dictionaries.","title":"@pnp/core/collections"},{"location":"v2/common/collections/#typedhash","text":"Interface used to described an object with string keys corresponding to values of type T export interface TypedHash { [key: string]: T; }","title":"TypedHash"},{"location":"v2/common/collections/#objecttomap","text":"Converts a plain object to a Map instance const map = objectToMap({ a: \"b\", c: \"d\"});","title":"objectToMap"},{"location":"v2/common/collections/#mergemaps","text":"Merges two or more maps, overwriting values with the same key. Last value in wins. const m1 = new Map(); const m2 = new Map(); const m3 = new Map(); const m4 = new Map(); const m = mergeMaps(m1, m2, m3, m4);","title":"mergeMaps"},{"location":"v2/common/custom-httpclientimpl/","text":"Custom HttpClientImpl \u00b6 This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation. It is possible you may need complete control over the sending and receiving of requests. Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation. The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. export interface HttpClientImpl { fetch(url: string, options: FetchOptions): Promise; } There is a single method \"fetch\" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. export interface FetchOptions { method?: string; headers?: HeadersInit | { [index: string]: string }; body?: BodyInit; mode?: string | RequestMode; credentials?: string | RequestCredentials; cache?: string | RequestCache; } So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read \ud83d\udc4d. Using Your Custom HttpClientImpl \u00b6 Once you have written your implementation using it on your requests is done by setting it in the global library configuration: import { setup } from \"@pnp/core\"; import { sp, Web } from \"@pnp/sp\"; import { MyAwesomeClient } from \"./awesomeclient\"; sp.setup({ sp: { fetchClientFactory: () => { return new MyAwesomeClient(); } } }); let w = new Web(\"{site url}\"); // this request will use your client. const result = await w.select(\"Title\")(); console.log(result); Subclassing is Better \u00b6 You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation. A FINAL NOTE \u00b6 Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.","title":"Custom HttpClientImpl"},{"location":"v2/common/custom-httpclientimpl/#custom-httpclientimpl","text":"This should be considered an advanced topic and creating a custom HttpClientImpl is not something you will likely need to do. Also, we don't offer support beyond this article for writing your own implementation. It is possible you may need complete control over the sending and receiving of requests. Before you get started read and understand the fetch specification as you are essentially writing a custom fetch implementation. The first step (second if you read the fetch spec as mentioned just above) is to understand the interface you need to implement, HttpClientImpl. export interface HttpClientImpl { fetch(url: string, options: FetchOptions): Promise; } There is a single method \"fetch\" which takes a url string and a set of options. These options can be just about anything but are constrained within the library to the FetchOptions interface. export interface FetchOptions { method?: string; headers?: HeadersInit | { [index: string]: string }; body?: BodyInit; mode?: string | RequestMode; credentials?: string | RequestCredentials; cache?: string | RequestCache; } So you will need to handle any of those options along with the provided url when sending your request. The library will expect your implementation to return a Promise that resolves to a Response defined by the fetch specification - which you've already read \ud83d\udc4d.","title":"Custom HttpClientImpl"},{"location":"v2/common/custom-httpclientimpl/#using-your-custom-httpclientimpl","text":"Once you have written your implementation using it on your requests is done by setting it in the global library configuration: import { setup } from \"@pnp/core\"; import { sp, Web } from \"@pnp/sp\"; import { MyAwesomeClient } from \"./awesomeclient\"; sp.setup({ sp: { fetchClientFactory: () => { return new MyAwesomeClient(); } } }); let w = new Web(\"{site url}\"); // this request will use your client. const result = await w.select(\"Title\")(); console.log(result);","title":"Using Your Custom HttpClientImpl"},{"location":"v2/common/custom-httpclientimpl/#subclassing-is-better","text":"You can of course inherit from one of the implementations available within the @pnp scope if you just need to say add a header or need to do something to every request sent. Perhaps some advanced logging. This approach will save you from needing to fully write a fetch implementation.","title":"Subclassing is Better"},{"location":"v2/common/custom-httpclientimpl/#a-final-note","text":"Whatever you do, do not write a client that uses a client id and secret and exposes them on the client side. Client Id and Secret should only ever be used on a server, never exposed to clients as anyone with those values has the full permissions granted to that id and secret.","title":"A FINAL NOTE"},{"location":"v2/common/libconfig/","text":"@pnp/core/libconfig \u00b6 Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications. ILibraryConfiguration Interface \u00b6 Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below export interface ILibraryConfiguration { /** * Allows caching to be global disabled, default: false */ globalCacheDisable?: boolean; /** * Defines the default store used by the usingCaching method, default: session */ defaultCachingStore?: \"session\" | \"local\"; /** * Defines the default timeout in seconds used by the usingCaching method, default 30 */ defaultCachingTimeoutSeconds?: number; /** * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval */ enableCacheExpiration?: boolean; /** * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) */ cacheExpirationIntervalMilliseconds?: number; /** * Used to supply the current context from an SPFx webpart to the library */ spfxContext?: any; } RuntimeConfigImpl \u00b6 The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method. assign \u00b6 The assign method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below \"Using RuntimeConfig within your application\". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. import { RuntimeConfig } from \"@pnp/core\"; // add your custom keys to the global configuration // note you can use object hashes as values RuntimeConfig.assign({ \"myKey1\": \"value 1\", \"myKey2\": { \"subKey\": \"sub value 1\", \"subKey2\": \"sub value 2\", }, }); // read your custom values const v = RuntimeConfig.get(\"myKey1\"); // \"value 1\" Using RuntimeConfig within your Application \u00b6 If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties import { ILibraryConfiguration, RuntimeConfig, ITypedHash } from \"@pnp/core\"; // first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because // TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. // create the interface of your properties // by creating this separately you allows others to compose your parts into their own config interface MyConfigurationPart { // you can create a grouped definition and access your settings as an object // keys can be optional or required as defined by your interface my?: { prop1?: string; prop2?: string; } // and/or define multiple top level properties (beware key collision) // it is good practice to use a unique prefix myProp1: string; myProp2: number; } // now create a combined interface interface MyConfiguration extends ILibraryConfiguration, MyConfigurationPart { } // now create a wrapper object and expose your properties class MyRuntimeConfigImpl { // exposing a nested property public get prop1(): ITypedHash { const myPart = RuntimeConfig.get(\"my\"); if (myPart !== null && typeof myPart !== \"undefined\" && typeof myPart.prop1 !== \"undefined\") { return myPart.prop1; } return {}; } // exposing a root level property public get myProp1(): string | null { let myProp1 = RuntimeConfig.get(\"myProp1\"); if (myProp1 === null) { myProp1 = \"some default value\"; } return myProp1; } setup(config: MyConfiguration): void { RuntimeConfig.assign(config); } } // create a single static instance of your impl class export let MyRuntimeConfig = new MyRuntimeConfigImpl(); Now in other files you can use and set your configuration with a typed interface and properties import { MyRuntimeConfig } from \"{location of module}\"; MyRuntimeConfig.setup({ my: { prop1: \"hello\", }, }); const value = MyRuntimeConfig.myProp1; // \"hello\"","title":"@pnp/core/libconfig"},{"location":"v2/common/libconfig/#pnpcorelibconfig","text":"Contains the shared classes and interfaces used to configure the libraries. These bases classes are expanded on in dependent libraries with the core configuration defined here. This module exposes an instance of the RuntimeConfigImpl class: RuntimeConfig. This configuration object can be referenced and contains the global configuration shared across the libraries. You can also extend the configuration for use within your own applications.","title":"@pnp/core/libconfig"},{"location":"v2/common/libconfig/#ilibraryconfiguration-interface","text":"Defines the shared configurable values used across the library as shown below. Each of these has a default value as shown below export interface ILibraryConfiguration { /** * Allows caching to be global disabled, default: false */ globalCacheDisable?: boolean; /** * Defines the default store used by the usingCaching method, default: session */ defaultCachingStore?: \"session\" | \"local\"; /** * Defines the default timeout in seconds used by the usingCaching method, default 30 */ defaultCachingTimeoutSeconds?: number; /** * If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval */ enableCacheExpiration?: boolean; /** * Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) */ cacheExpirationIntervalMilliseconds?: number; /** * Used to supply the current context from an SPFx webpart to the library */ spfxContext?: any; }","title":"ILibraryConfiguration Interface"},{"location":"v2/common/libconfig/#runtimeconfigimpl","text":"The class which implements the runtime configuration management as well as sets the default values used within the library. At its heart lies a Dictionary used to track the configuration values. The keys will match the values in the interface or plain object passed to the extend method.","title":"RuntimeConfigImpl"},{"location":"v2/common/libconfig/#assign","text":"The assign method is used to add configuration to the global configuration instance. You can pass it any plain object with string keys and those values will be added. Any existing values will be overwritten based on the keys. Last value in wins. For a more detailed scenario of using the RuntimeConfig instance in your own application please see the section below \"Using RuntimeConfig within your application\". Note there are no methods to remove/clear the global config as it should be considered fairly static as frequent updates may have unpredictable side effects as it is a global shared object. Generally it should be set at the start of your application. import { RuntimeConfig } from \"@pnp/core\"; // add your custom keys to the global configuration // note you can use object hashes as values RuntimeConfig.assign({ \"myKey1\": \"value 1\", \"myKey2\": { \"subKey\": \"sub value 1\", \"subKey2\": \"sub value 2\", }, }); // read your custom values const v = RuntimeConfig.get(\"myKey1\"); // \"value 1\"","title":"assign"},{"location":"v2/common/libconfig/#using-runtimeconfig-within-your-application","text":"If you have a set of properties you will access very frequently it may be desirable to implement your own configuration object and expose those values as properties. To do so you will need to create an interface for your configuration (optional) and a wrapper class for RuntimeConfig to expose your properties import { ILibraryConfiguration, RuntimeConfig, ITypedHash } from \"@pnp/core\"; // first we create our own interface by extending LibraryConfiguration. This allows your class to accept all the values with correct type checking. Note, because // TypeScript allows you to extend from multiple interfaces you can build a complex configuration definition from many sub definitions. // create the interface of your properties // by creating this separately you allows others to compose your parts into their own config interface MyConfigurationPart { // you can create a grouped definition and access your settings as an object // keys can be optional or required as defined by your interface my?: { prop1?: string; prop2?: string; } // and/or define multiple top level properties (beware key collision) // it is good practice to use a unique prefix myProp1: string; myProp2: number; } // now create a combined interface interface MyConfiguration extends ILibraryConfiguration, MyConfigurationPart { } // now create a wrapper object and expose your properties class MyRuntimeConfigImpl { // exposing a nested property public get prop1(): ITypedHash { const myPart = RuntimeConfig.get(\"my\"); if (myPart !== null && typeof myPart !== \"undefined\" && typeof myPart.prop1 !== \"undefined\") { return myPart.prop1; } return {}; } // exposing a root level property public get myProp1(): string | null { let myProp1 = RuntimeConfig.get(\"myProp1\"); if (myProp1 === null) { myProp1 = \"some default value\"; } return myProp1; } setup(config: MyConfiguration): void { RuntimeConfig.assign(config); } } // create a single static instance of your impl class export let MyRuntimeConfig = new MyRuntimeConfigImpl(); Now in other files you can use and set your configuration with a typed interface and properties import { MyRuntimeConfig } from \"{location of module}\"; MyRuntimeConfig.setup({ my: { prop1: \"hello\", }, }); const value = MyRuntimeConfig.myProp1; // \"hello\"","title":"Using RuntimeConfig within your Application"},{"location":"v2/common/netutil/","text":"@pnp/core/net \u00b6 This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces are described below (many have no use outside the library) as well as several classes. Interfaces \u00b6 HttpClientImpl \u00b6 Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method \"fetch\" takes a URL and options. It returns a Promise . Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed. RequestClient \u00b6 An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the underlying HttpClientImpl fetch method. Classes \u00b6 This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl. FetchClient \u00b6 Basic implementation that calls the global (window) fetch method with no additional processing. import { FetchClient } from \"@pnp/core\"; const client = new FetchClient(); client.fetch(\"{url}\", {}); BearerTokenFetchClient \u00b6 A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string. import { BearerTokenFetchClient } from \"@pnp/core\"; const client = new BearerTokenFetchClient(\"{authentication token}\"); client.fetch(\"{url}\", {});","title":"@pnp/core/net"},{"location":"v2/common/netutil/#pnpcorenet","text":"This module contains a set of classes and interfaces used to characterize shared http interactions and configuration of the libraries. Some of the interfaces are described below (many have no use outside the library) as well as several classes.","title":"@pnp/core/net"},{"location":"v2/common/netutil/#interfaces","text":"","title":"Interfaces"},{"location":"v2/common/netutil/#httpclientimpl","text":"Defines an implementation of an Http Client within the context of @pnp. This being a class with a a single method \"fetch\" takes a URL and options. It returns a Promise . Used primarily with the shared request pipeline to define the client used to make the actual request. You can write your own custom implementation if needed.","title":"HttpClientImpl"},{"location":"v2/common/netutil/#requestclient","text":"An abstraction that contains specific methods related to each of the primary request methods get, post, patch, delete as well as fetch and fetchRaw. The difference between fetch and fetchRaw is that a client may include additional logic or processing in fetch, where fetchRaw should be a direct call to the underlying HttpClientImpl fetch method.","title":"RequestClient"},{"location":"v2/common/netutil/#classes","text":"This module export two classes of note, FetchClient and BearerTokenFetchClient. Both implement HttpClientImpl.","title":"Classes"},{"location":"v2/common/netutil/#fetchclient","text":"Basic implementation that calls the global (window) fetch method with no additional processing. import { FetchClient } from \"@pnp/core\"; const client = new FetchClient(); client.fetch(\"{url}\", {});","title":"FetchClient"},{"location":"v2/common/netutil/#bearertokenfetchclient","text":"A simple implementation that takes a provided authentication token and adds the Authentication Bearer header to the request. No other processing is done and the token is treated as a static string. import { BearerTokenFetchClient } from \"@pnp/core\"; const client = new BearerTokenFetchClient(\"{authentication token}\"); client.fetch(\"{url}\", {});","title":"BearerTokenFetchClient"},{"location":"v2/common/storage/","text":"@pnp/core/storage \u00b6 This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below. PnPClientStorage \u00b6 The main export of this module, contains properties representing local and session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); const myvalue = storage.local.get(\"mykey\"); PnPClientStorageWrapper \u00b6 Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // get a value from storage const value = storage.local.get(\"mykey\"); // put a value into storage storage.local.put(\"mykey2\", \"my value\"); // put a value into storage with an expiration storage.local.put(\"mykey2\", \"my value\", new Date()); // put a simple object into storage // because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects storage.local.put(\"mykey3\", { key: \"value\", key2: \"value2\", }); // remove a value from storage storage.local.delete(\"mykey3\"); // get an item or add it if it does not exist // returns a promise in case you need time to get the value for storage // optionally takes a third parameter specifying the expiration storage.local.getOrPut(\"mykey4\", () => { return Promise.resolve(\"value\"); }); // delete expired items storage.local.deleteExpired(); Cache Expiration \u00b6 The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // session storage storage.session.deleteExpired(); // local storage storage.local.deleteExpired(); // this returns a promise, so you can perform some activity after the expired items are removed: storage.local.deleteExpired().then(_ => { // init my application }); The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. import { setup } from \"@pnp/core\"; setup({ enableCacheExpiration: true, cacheExpirationIntervalMilliseconds: 1000, // optional });","title":"@pnp/core/storage"},{"location":"v2/common/storage/#pnpcorestorage","text":"This module provides a thin wrapper over the browser storage options, local and session. If neither option is available it shims storage with a non-persistent in memory polyfill. Optionally through configuration you can activate expiration. Sample usage is shown below.","title":"@pnp/core/storage"},{"location":"v2/common/storage/#pnpclientstorage","text":"The main export of this module, contains properties representing local and session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); const myvalue = storage.local.get(\"mykey\");","title":"PnPClientStorage"},{"location":"v2/common/storage/#pnpclientstoragewrapper","text":"Each of the storage locations (session and local) are wrapped with this helper class. You can use it directly, but generally it would be used from an instance of PnPClientStorage as shown below. These examples all use local storage, the operations are identical for session storage. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // get a value from storage const value = storage.local.get(\"mykey\"); // put a value into storage storage.local.put(\"mykey2\", \"my value\"); // put a value into storage with an expiration storage.local.put(\"mykey2\", \"my value\", new Date()); // put a simple object into storage // because JSON.stringify is used to package the object we do NOT do a deep rehydration of stored objects storage.local.put(\"mykey3\", { key: \"value\", key2: \"value2\", }); // remove a value from storage storage.local.delete(\"mykey3\"); // get an item or add it if it does not exist // returns a promise in case you need time to get the value for storage // optionally takes a third parameter specifying the expiration storage.local.getOrPut(\"mykey4\", () => { return Promise.resolve(\"value\"); }); // delete expired items storage.local.deleteExpired();","title":"PnPClientStorageWrapper"},{"location":"v2/common/storage/#cache-expiration","text":"The ability remove of expired items based on a configured timeout can help if the cache is filling up. This can be accomplished in two ways. The first is to explicitly call the new deleteExpired method on the cache you wish to clear. A suggested usage is to add this into your page init code as clearing expired items once per page load is likely sufficient. import { PnPClientStorage } from \"@pnp/core\"; const storage = new PnPClientStorage(); // session storage storage.session.deleteExpired(); // local storage storage.local.deleteExpired(); // this returns a promise, so you can perform some activity after the expired items are removed: storage.local.deleteExpired().then(_ => { // init my application }); The second method is to enable automated cache expiration through global config. Setting the enableCacheExpiration property to true will enable the timer. Optionally you can set the interval at which the cache is checked via the cacheExpirationIntervalMilliseconds property, by default 750 milliseconds is used. We enforce a minimum of 300 milliseconds as this functionality is enabled via setTimeout and there is little value in having an excessive number of cache checks. This method is more appropriate for a single page application where the page is infrequently reloaded and many cached operations are performed. There is no advantage to enabling cache expiration unless you are experiencing cache storage space pressure in a long running page - and you may see a performance hit due to the use of setTimeout. import { setup } from \"@pnp/core\"; setup({ enableCacheExpiration: true, cacheExpirationIntervalMilliseconds: 1000, // optional });","title":"Cache Expiration"},{"location":"v2/common/util/","text":"@pnp/core/util \u00b6 This module contains utility methods that you can import individually from the common library. import { getRandomString, } from \"@pnp/core\"; // use from individually imported method console.log(getRandomString(10)); assign \u00b6 Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing properties. import { assign } from \"@pnp/core\"; let obj1 = { prop: 1, prop2: 2, }; const obj2 = { prop: 4, prop3: 9, }; const example1 = assign(obj1, obj2); // example1 = { prop: 4, prop2: 2, prop3: 9 } //noOverwrite = true stops overwriting existing properties const example2 = assign(obj1, obj2, true); // example2 = { prop: 1, prop2: 2, prop3: 9 } combine \u00b6 Combines any number of paths, normalizing the slashes as required import { combine } from \"@pnp/core\"; // \"https://microsoft.com/something/more\" const paths = combine(\"https://microsoft.com\", \"something\", \"more\"); // \"also/works/with/relative\" const paths2 = combine(\"/also/\", \"/works\", \"with/\", \"/relative\\\\\"); dateAdd \u00b6 Manipulates a date, please see the Stack Overflow discussion from where this method was taken. import { dateAdd } from \"@pnp/core\"; const testDate = new Date(); dateAdd(testDate,'minute',10); getCtxCallback \u00b6 Gets a callback function which will maintain context across async calls. import { getCtxCallback } from \"@pnp/core\"; const contextThis = { myProp: 6, }; function theFunction() { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp; } const callback = getCtxCallback(contextThis, theFunction); callback(); // returns 6 // You can also supply additional parameters if needed function theFunction2(g: number) { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp + g; } const callback2 = getCtxCallback(contextThis, theFunction2, 4); callback2(); // returns 10 (6 + 4) getGUID \u00b6 Creates a random guid, please see the Stack Overflow discussion from where this method was taken. import { getGUID } from \"@pnp/core\"; const newGUID = getGUID(); getRandomString \u00b6 Gets a random string consisting of the number of characters requested. import { getRandomString } from \"@pnp/core\"; const randomString = getRandomString(10); hOP \u00b6 Shortcut for Object.hasOwnProperty. Determines if an object has a specified property. import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { //Checks to see if the error object has a property called isHttpRequestError. Returns a bool. if (hOP(e, \"isHttpRequestError\")) { // Handle this type or error } else { // not an HttpRequestError so we do something else } } isArray \u00b6 Determines if a supplied variable represents an array. import { isArray } from \"@pnp/core\"; let x:String[] = [1,2,3]]; if (isArray(x)){ console.log(\"I am an array\"); }else{ console.log(\"I am not an array\"); } isFunc \u00b6 Determines if a supplied variable represents a function. import { isFunc } from \"@pnp/core\"; public testFunction() { console.log(\"test function\"); return } if (isFunc(testFunction)){ console.log(\"this is a function\"); testFunction(); } isUrlAbsolute \u00b6 Determines if a supplied url is absolute and returns true; otherwise returns false. import { isUrlAbsolute } from \"@pnp/core\"; const webPath = 'https://{tenant}.sharepoint.com/sites/dev/'; if (isUrlAbsolute(webPath)){ console.log(\"URL is absolute\"); }else{ console.log(\"URL is not absolute\"); } objectDefinedNotNull \u00b6 Determines if an object is defined and not null. import { objectDefinedNotNull } from \"@pnp/core\"; let obj = { prop: 1 }; if (objectDefinedNotNull(obj)){ console.log(\"Not null\"); }else{ console.log(\"Null\"); } stringIsNullOrEmpty \u00b6 Determines if a supplied string is null or empty. import { stringIsNullOrEmpty } from \"@pnp/core\"; let x:String = \"hello\"; if (stringIsNullOrEmpty(x)){ console.log(\"Null or empty\"); }else{ console.log(\"Not null or empty\"); } Removed \u00b6 Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods below for use in your projects should you require. /** * Loads a stylesheet into the current page * * @param path The url to the stylesheet * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues */ public static loadStylesheet(path: string, avoidCache: boolean): void { if (avoidCache) { path += \"?\" + encodeURIComponent((new Date()).getTime().toString()); } const head = document.getElementsByTagName(\"head\"); if (head.length > 0) { const e = document.createElement(\"link\"); head[0].appendChild(e); e.setAttribute(\"type\", \"text/css\"); e.setAttribute(\"rel\", \"stylesheet\"); e.setAttribute(\"href\", path); } } /** * Tests if a url param exists * * @param name The name of the url parameter to check */ public static urlParamExists(name: string): boolean { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); return regex.test(location.search); } /** * Gets a url param value by name * * @param name The name of the parameter for which we want the value */ public static getUrlParamByName(name: string): string { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); const results = regex.exec(location.search); return results == null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \")); } /** * Gets a url param by name and attempts to parse a bool value * * @param name The name of the parameter for which we want the boolean value */ public static getUrlParamBoolByName(name: string): boolean { const p = this.getUrlParamByName(name); const isFalse = (p === \"\" || /false|0/i.test(p)); return !isFalse; } /** * Inserts the string s into the string target as the index specified by index * * @param target The string into which we will insert s * @param index The location in target to insert s (zero based) * @param s The string to insert into target at position index */ public static stringInsert(target: string, index: number, s: string): string { if (index > 0) { return target.substring(0, index) + s + target.substring(index, target.length); } return s + target; }","title":"@pnp/core/util"},{"location":"v2/common/util/#pnpcoreutil","text":"This module contains utility methods that you can import individually from the common library. import { getRandomString, } from \"@pnp/core\"; // use from individually imported method console.log(getRandomString(10));","title":"@pnp/core/util"},{"location":"v2/common/util/#assign","text":"Merges a source object's own enumerable properties into a single target object. Similar to Object.assign, but allows control of overwriting of existing properties. import { assign } from \"@pnp/core\"; let obj1 = { prop: 1, prop2: 2, }; const obj2 = { prop: 4, prop3: 9, }; const example1 = assign(obj1, obj2); // example1 = { prop: 4, prop2: 2, prop3: 9 } //noOverwrite = true stops overwriting existing properties const example2 = assign(obj1, obj2, true); // example2 = { prop: 1, prop2: 2, prop3: 9 }","title":"assign"},{"location":"v2/common/util/#combine","text":"Combines any number of paths, normalizing the slashes as required import { combine } from \"@pnp/core\"; // \"https://microsoft.com/something/more\" const paths = combine(\"https://microsoft.com\", \"something\", \"more\"); // \"also/works/with/relative\" const paths2 = combine(\"/also/\", \"/works\", \"with/\", \"/relative\\\\\");","title":"combine"},{"location":"v2/common/util/#dateadd","text":"Manipulates a date, please see the Stack Overflow discussion from where this method was taken. import { dateAdd } from \"@pnp/core\"; const testDate = new Date(); dateAdd(testDate,'minute',10);","title":"dateAdd"},{"location":"v2/common/util/#getctxcallback","text":"Gets a callback function which will maintain context across async calls. import { getCtxCallback } from \"@pnp/core\"; const contextThis = { myProp: 6, }; function theFunction() { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp; } const callback = getCtxCallback(contextThis, theFunction); callback(); // returns 6 // You can also supply additional parameters if needed function theFunction2(g: number) { // \"this\" within this function will be the context object supplied // in this case the variable contextThis, so myProp will exist return this.myProp + g; } const callback2 = getCtxCallback(contextThis, theFunction2, 4); callback2(); // returns 10 (6 + 4)","title":"getCtxCallback"},{"location":"v2/common/util/#getguid","text":"Creates a random guid, please see the Stack Overflow discussion from where this method was taken. import { getGUID } from \"@pnp/core\"; const newGUID = getGUID();","title":"getGUID"},{"location":"v2/common/util/#getrandomstring","text":"Gets a random string consisting of the number of characters requested. import { getRandomString } from \"@pnp/core\"; const randomString = getRandomString(10);","title":"getRandomString"},{"location":"v2/common/util/#hop","text":"Shortcut for Object.hasOwnProperty. Determines if an object has a specified property. import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { //Checks to see if the error object has a property called isHttpRequestError. Returns a bool. if (hOP(e, \"isHttpRequestError\")) { // Handle this type or error } else { // not an HttpRequestError so we do something else } }","title":"hOP"},{"location":"v2/common/util/#isarray","text":"Determines if a supplied variable represents an array. import { isArray } from \"@pnp/core\"; let x:String[] = [1,2,3]]; if (isArray(x)){ console.log(\"I am an array\"); }else{ console.log(\"I am not an array\"); }","title":"isArray"},{"location":"v2/common/util/#isfunc","text":"Determines if a supplied variable represents a function. import { isFunc } from \"@pnp/core\"; public testFunction() { console.log(\"test function\"); return } if (isFunc(testFunction)){ console.log(\"this is a function\"); testFunction(); }","title":"isFunc"},{"location":"v2/common/util/#isurlabsolute","text":"Determines if a supplied url is absolute and returns true; otherwise returns false. import { isUrlAbsolute } from \"@pnp/core\"; const webPath = 'https://{tenant}.sharepoint.com/sites/dev/'; if (isUrlAbsolute(webPath)){ console.log(\"URL is absolute\"); }else{ console.log(\"URL is not absolute\"); }","title":"isUrlAbsolute"},{"location":"v2/common/util/#objectdefinednotnull","text":"Determines if an object is defined and not null. import { objectDefinedNotNull } from \"@pnp/core\"; let obj = { prop: 1 }; if (objectDefinedNotNull(obj)){ console.log(\"Not null\"); }else{ console.log(\"Null\"); }","title":"objectDefinedNotNull"},{"location":"v2/common/util/#stringisnullorempty","text":"Determines if a supplied string is null or empty. import { stringIsNullOrEmpty } from \"@pnp/core\"; let x:String = \"hello\"; if (stringIsNullOrEmpty(x)){ console.log(\"Null or empty\"); }else{ console.log(\"Not null or empty\"); }","title":"stringIsNullOrEmpty"},{"location":"v2/common/util/#removed","text":"Some methods that were no longer used internally by the @pnp libraries have been removed. You can find the source for those methods below for use in your projects should you require. /** * Loads a stylesheet into the current page * * @param path The url to the stylesheet * @param avoidCache If true a value will be appended as a query string to avoid browser caching issues */ public static loadStylesheet(path: string, avoidCache: boolean): void { if (avoidCache) { path += \"?\" + encodeURIComponent((new Date()).getTime().toString()); } const head = document.getElementsByTagName(\"head\"); if (head.length > 0) { const e = document.createElement(\"link\"); head[0].appendChild(e); e.setAttribute(\"type\", \"text/css\"); e.setAttribute(\"rel\", \"stylesheet\"); e.setAttribute(\"href\", path); } } /** * Tests if a url param exists * * @param name The name of the url parameter to check */ public static urlParamExists(name: string): boolean { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); return regex.test(location.search); } /** * Gets a url param value by name * * @param name The name of the parameter for which we want the value */ public static getUrlParamByName(name: string): string { name = name.replace(/[\\[]/, \"\\\\[\").replace(/[\\]]/, \"\\\\]\"); const regex = new RegExp(\"[\\\\?&]\" + name + \"=([^&#]*)\"); const results = regex.exec(location.search); return results == null ? \"\" : decodeURIComponent(results[1].replace(/\\+/g, \" \")); } /** * Gets a url param by name and attempts to parse a bool value * * @param name The name of the parameter for which we want the boolean value */ public static getUrlParamBoolByName(name: string): boolean { const p = this.getUrlParamByName(name); const isFalse = (p === \"\" || /false|0/i.test(p)); return !isFalse; } /** * Inserts the string s into the string target as the index specified by index * * @param target The string into which we will insert s * @param index The location in target to insert s (zero based) * @param s The string to insert into target at position index */ public static stringInsert(target: string, index: number, s: string): string { if (index > 0) { return target.substring(0, index) + s + target.substring(index, target.length); } return s + target; }","title":"Removed"},{"location":"v2/concepts/configuration/","text":"PnPjs Configuration \u00b6 This article describes the configuration architecture used by the library as well as the settings available. Starting with version 2.1.0 we updated our configuration design to support the ability to isolate settings to individual objects. The first part of this article discusses the newer design, you can read about the pre v2.1.0 configuration further down. Post v2.1.0 \u00b6 Architecture \u00b6 Starting from v2.1.0 we have modified our configuration design to allow for configuring individual queryable objects. Backward Compatibility \u00b6 If you have no need to use the isolated runtimes introduced in 2.1.0 then you should see no change in library behavior from prior versions. You can continue to refer to the pre v2.1.0 configuration section - and if you see any issues please let us know. All of the available settings as described below remain, unchanged. If you previously used our internal configuration classes directly RuntimeConfigImpl, SPRuntimeConfigImpl, or GraphRuntimeConfigImpl they no longer exist. We do not consider this a breaking change as they were meant to be internal and their direct use was not documented. This includes the concrete default instances RuntimeConfig, SPRuntimeConfig, and GraphRuntimeConfig. Isolated Runtimes \u00b6 You can create an isolated runtime when using either the sp or graph libraries. What this does is create an isolated set of properties and behaviors specific to a given fluent chain. Have a look at this basic example below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuration applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuration applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the web at https://mytenant.sharepoint.com/ const web1 = await sp.web(); // details for the web at https://mytenant.sharepoint.com/sites/dev const web2 = await isolatedSP.web(); This configuration is supplied to all objects down a given fluent chain: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuraiton applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuraiton applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the lists at https://mytenant.sharepoint.com/ const lists1 = await sp.web.lists(); // details for the lists at https://mytenant.sharepoint.com/sites/dev const lists2 = await isolatedSP.web.lists(); createIsolated \u00b6 The createIsolated method is used to establish the isolated runtime for a given instance of either the sp or graph libraries. Once created it is no longer connected to the default instance and if you have common settings that must be updated you would need to update them across each isolated instance, this is by design. Currently sp and graph createIsolated methods accept the same init, but we have broken them out to make thing clear. All properties of the init object are optional. Any properties provided will overwrite those cloned from the default if cloneGlobal is true. If cloneGlobal is false you start with an empty config containing only the core defaults . sp.createIsolated \u00b6 import { sp, ISPConfiguration } from \"@pnp/sp\"; // accept all the defaults, will clone any settings from sp const isolatedSP = await sp.createIsolated(); // - specify all the config options, using the ISPConfiguration interface to type the config // - setting baseUrl in the root is equivelent to setting it with sp: { baseUrl: }, it is provided as a shortcut as this seemed to be a common use case // - if you set them both the baseUrl in the root will be used. // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedSP = await sp.createIsolated({ baseUrl: \"https://mytenant.sharepoint.com\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, sp: { baseUrl: \"https://mytenant.sharepoint.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, }); Defaults Name Default baseUrl \"\" cloneGlobal true config {} options {} graph.createIsolated \u00b6 import { graph, IGraphConfiguration } from \"@pnp/graph\"; // - specify all the config options, using the IGraphConfiguration interface to type the config // - setting baseUrl in the root is restricted to \"v1.0\" or \"beta\". If you need to specify a different absolute url should use config.graph.baseUrl // - in practice you should use one or the other. You can always swap Graph api version using IGraphQueryable.setEndpoint // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedGraph = await graph.createIsolated({ baseUrl: \"v1.0\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, graph: { baseUrl: \"https://graph.microsoft.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, }); Defaults \u00b6 name Default baseUrl \"v1.0\" cloneGlobal true config {} options {} Additional Examples \u00b6 MSAL with Node multiple site requests \u00b6 MSAL Support Added in 2.0.11 In this example you can see how you can setup the MSAL client once and then set a different baseUrl for an isolated instance. More information specific to setting up the MSAL client is available . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev2/\", }, }, }); Node multiple site requests \u00b6 Isolated configuration was most requested for scenarios in node where you need to access information in multiple sites. This example shows setting up the global configuration and then creating an isolated config with only the baseUrl updated. import { SPFetchClient } from \"@pnp/nodejs\"; import { ISPConfigurationPart, sp } from \"@pnp/sp\"; sp.setup({ cacheExpirationIntervalMilliseconds: 1000, defaultCachingStore: \"local\", sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/\", \"id\", \"secret\"); }, headers: { \"X-MyRequiredHeader\": \"SomeValue\", \"X-MyRequiredHeader2\": \"SomeValue\", }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/site/dev\", \"id\", \"secret\"); }, }, }, }); Batching \u00b6 All batching functionality works as expected, but you must take care to only associate requests from the same isolated instance as you create the batch. Mixing requests across isolation boundaries is not supported. This applies to sp and graph batching. sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"url1\", \"id\", \"secret\"); }, }, }); const isolated = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"url2\", \"id\", \"secret\"); }, }, }, }); const batch1 = sp.createBatch(); sp.web.lists.select(\"Title\").top(3).inBatch(batch1)().then(r => console.log(`here 1: ${JSON.stringify(r, null, 2)}`)); sp.web.select(\"Title\").inBatch(batch1)().then(r => console.log(`here 2: ${JSON.stringify(r, null, 2)}`)); await batch1.execute(); const batch2 = isolated.createBatch(); isolated.web.lists.select(\"Title\").top(3).inBatch(batch2)().then(r => console.log(`here 3: ${JSON.stringify(r, null, 2)}`)); isolated.web.select(\"Title\").inBatch(batch2)().then(r => console.log(`here 4: ${JSON.stringify(r, null, 2)}`)); await batch2.execute(); IE11 Mode \u00b6 The IE11 mode setting is always global. There is no scenario we care to support where once instance needs to run in ie11 mode and another does not. Your code either does or does not run in ie11. Prior to v2.1.0 \u00b6 Architecture \u00b6 PnPjs uses an additive configuration design with multiple libraries sharing a single global configuration instance. If you need non-global configuration please see this section . There are three ways to access the setup functionality - through either the common, sp, or graph library's setup method. While the configuration is global the various methods have different typing on their input parameter. You can review the libconfig article for more details on storing your own configuration. Common Configuration \u00b6 The common libary's setup method takes parameters defined by ILibraryConfiguration . The properties and their defaults are listed below, followed by a code sample. You can call setup multiple times and any new values will be added to the existing configuration or replace the previous value if one existed. All values are optional. Name Description Default defaultCachingStore Where will PnPjs store cached data by default (session or local) session defaultCachingTimeoutSeconds The global default value used for cached data timeouts in seconds 60 globalCacheDisable Provides a way to globally within PnPjs disable all caching false enableCacheExpiration If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval false cacheExpirationIntervalMilliseconds Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) 750 spfxContext When running in SPFx the current context should always be supplied to PnPjs when available null ie11 If true the library downgrades functionality to work in IE11 false For more information on setting up in SPFx please see the authentication section For more details on ie11 mode please see the topic article import { setup } from \"@pnp/core\"; // called before other code setup({ cacheExpirationIntervalMilliseconds: 15000, defaultCachingStore: \"local\", defaultCachingTimeoutSeconds: 600, enableCacheExpiration: true, globalCacheDisable: false, ie11: false, spfxContext: this.context, // if in SPFx, otherwise leave it out }); SP Configuration \u00b6 The sp library's configuration is defined by the ISPConfiguration interface which extends ILibraryConfiguration. All of the sp values are contained in a top level property named \"sp\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { sp } from \"@pnp/sp\"; import { SPFxAdalClient } from \"@pnp/core\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration sp.setup({ ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", fetchClientFactory: () => { return new SPFxAdalClient(this.context); }, headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, }); SharePoint Framework \u00b6 You can optionally supply only the SPFx context to the sp configure method. import { sp } from \"@pnp/sp\"; // in SPFx only sp.setup(this.context); Graph Configuration \u00b6 The graph configuration works exactly the same as the sp configuration but is defined by the IGraphConfiguration interface which extends ILibraryConfiguration. All of the graph values are contained in a top level property named \"graph\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. ( Added in 2.0.8 ) none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { graph } from \"@pnp/graph\"; import { MsalClientSetup } from \"@pnp/msaljsclient\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration graph.setup({ ie11: false, graph: { // we set the GCC url baseUrl: \"https://graph.microsoft.us\", fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, }); SharePoint Framework \u00b6 You can optionally supply only the SPFx context to the graph configure method. We will attempt to set the baseUrl property from the context - but if that is failing in your environment and you need to call a special cloud (i.e. graph.microsoft.us) please set the baseUrl property. import { graph } from \"@pnp/graph\"; // in SPFx only graph.setup(this.context); Configure Everything At Once \u00b6 In some cases you might want to configure everything in one go. Because the configuration is stored in a single location you can use the common library's setup method and adjust the typings to ensure you are using the correct property names while only having to setup things with a single call. In versions before 2.0.8 ISPConfigurationPart, IGraphConfigurationPart, and ILibraryConfiguration incorrectly were missing the \"I\" prefix. That was fixed in 2.0.8 - but note if you are using an older version of the library you'll need to use the old names. Everything else in the below example works as expected. import { ISPConfigurationPart } from \"@pnp/sp\"; import { IGraphConfigurationPart } from \"@pnp/graph\"; import { ILibraryConfiguration, setup } from \"@pnp/core\"; // you could also include your custom configuration parts export interface AllConfig extends ILibraryConfiguration, ISPConfigurationPart, IGraphConfigurationPart { } // create a single big configuration entry const config: AllConfig = { graph: { baseUrl: \"https://graph.microsoft.us\", }, ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", }, }; setup(config);","title":"PnPjs Configuration"},{"location":"v2/concepts/configuration/#pnpjs-configuration","text":"This article describes the configuration architecture used by the library as well as the settings available. Starting with version 2.1.0 we updated our configuration design to support the ability to isolate settings to individual objects. The first part of this article discusses the newer design, you can read about the pre v2.1.0 configuration further down.","title":"PnPjs Configuration"},{"location":"v2/concepts/configuration/#post-v210","text":"","title":"Post v2.1.0"},{"location":"v2/concepts/configuration/#architecture","text":"Starting from v2.1.0 we have modified our configuration design to allow for configuring individual queryable objects.","title":"Architecture"},{"location":"v2/concepts/configuration/#backward-compatibility","text":"If you have no need to use the isolated runtimes introduced in 2.1.0 then you should see no change in library behavior from prior versions. You can continue to refer to the pre v2.1.0 configuration section - and if you see any issues please let us know. All of the available settings as described below remain, unchanged. If you previously used our internal configuration classes directly RuntimeConfigImpl, SPRuntimeConfigImpl, or GraphRuntimeConfigImpl they no longer exist. We do not consider this a breaking change as they were meant to be internal and their direct use was not documented. This includes the concrete default instances RuntimeConfig, SPRuntimeConfig, and GraphRuntimeConfig.","title":"Backward Compatibility"},{"location":"v2/concepts/configuration/#isolated-runtimes","text":"You can create an isolated runtime when using either the sp or graph libraries. What this does is create an isolated set of properties and behaviors specific to a given fluent chain. Have a look at this basic example below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuration applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuration applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the web at https://mytenant.sharepoint.com/ const web1 = await sp.web(); // details for the web at https://mytenant.sharepoint.com/sites/dev const web2 = await isolatedSP.web(); This configuration is supplied to all objects down a given fluent chain: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // create an isolated sp root instance const isolatedSP = await sp.createIsolated(); // this configuraiton applies to all objects created from \"sp\" sp.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/\", }, }); // this configuraiton applies to all objects created from \"isolatedSP\" isolatedSP.setup({ sp: { baseUrl: \"https://mytenant.sharepoint.com/sites/dev\", }, }); // details for the lists at https://mytenant.sharepoint.com/ const lists1 = await sp.web.lists(); // details for the lists at https://mytenant.sharepoint.com/sites/dev const lists2 = await isolatedSP.web.lists();","title":"Isolated Runtimes"},{"location":"v2/concepts/configuration/#createisolated","text":"The createIsolated method is used to establish the isolated runtime for a given instance of either the sp or graph libraries. Once created it is no longer connected to the default instance and if you have common settings that must be updated you would need to update them across each isolated instance, this is by design. Currently sp and graph createIsolated methods accept the same init, but we have broken them out to make thing clear. All properties of the init object are optional. Any properties provided will overwrite those cloned from the default if cloneGlobal is true. If cloneGlobal is false you start with an empty config containing only the core defaults .","title":"createIsolated"},{"location":"v2/concepts/configuration/#spcreateisolated","text":"import { sp, ISPConfiguration } from \"@pnp/sp\"; // accept all the defaults, will clone any settings from sp const isolatedSP = await sp.createIsolated(); // - specify all the config options, using the ISPConfiguration interface to type the config // - setting baseUrl in the root is equivelent to setting it with sp: { baseUrl: }, it is provided as a shortcut as this seemed to be a common use case // - if you set them both the baseUrl in the root will be used. // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedSP = await sp.createIsolated({ baseUrl: \"https://mytenant.sharepoint.com\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, sp: { baseUrl: \"https://mytenant.sharepoint.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, }); Defaults Name Default baseUrl \"\" cloneGlobal true config {} options {}","title":"sp.createIsolated"},{"location":"v2/concepts/configuration/#graphcreateisolated","text":"import { graph, IGraphConfiguration } from \"@pnp/graph\"; // - specify all the config options, using the IGraphConfiguration interface to type the config // - setting baseUrl in the root is restricted to \"v1.0\" or \"beta\". If you need to specify a different absolute url should use config.graph.baseUrl // - in practice you should use one or the other. You can always swap Graph api version using IGraphQueryable.setEndpoint // - you can set some or all of the settings in config and if you clone from the global the ones you specify will overwrite the cloned values // - for example your global config can specify everything and your isolated config could specify a different fetchClientFactory, see node example below const isolatedGraph = await graph.createIsolated({ baseUrl: \"v1.0\", cloneGlobal: false, config: { cacheExpirationIntervalMilliseconds: 1000, graph: { baseUrl: \"https://graph.microsoft.com\", fetchClientFactory: () => void(0), headers: { \"X-AnotherHeader\": \"54321\", }, }, spfxContext: this.context, // only valid within SPFx }, options: { headers: { \"X-SomeHeader\": \"12345\", }, }, });","title":"graph.createIsolated"},{"location":"v2/concepts/configuration/#defaults","text":"name Default baseUrl \"v1.0\" cloneGlobal true config {} options {}","title":"Defaults"},{"location":"v2/concepts/configuration/#additional-examples","text":"","title":"Additional Examples"},{"location":"v2/concepts/configuration/#msal-with-node-multiple-site-requests","text":"MSAL Support Added in 2.0.11 In this example you can see how you can setup the MSAL client once and then set a different baseUrl for an isolated instance. More information specific to setting up the MSAL client is available . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { readFileSync } from \"fs\"; // read in our private key const buffer = readFileSync(\"c:/temp/key.pem\"); // configure node options sp.setup({ sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev/\", fetchClientFactory: () => { return new MsalFetchClient({ auth: { authority: \"https://login.microsoftonline.com/{tenant id or common}\", clientCertificate: { thumbprint: \"{certificate thumbprint, displayed in AAD}\", privateKey: buffer.toString(), }, clientId: \"{client id}\", } }, [\"https://{my tenant}.sharepoint.com/.default\"]); // you must set the scope for SharePoint access }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { baseUrl: \"https://{my tenant}.sharepoint.com/sites/dev2/\", }, }, });","title":"MSAL with Node multiple site requests"},{"location":"v2/concepts/configuration/#node-multiple-site-requests","text":"Isolated configuration was most requested for scenarios in node where you need to access information in multiple sites. This example shows setting up the global configuration and then creating an isolated config with only the baseUrl updated. import { SPFetchClient } from \"@pnp/nodejs\"; import { ISPConfigurationPart, sp } from \"@pnp/sp\"; sp.setup({ cacheExpirationIntervalMilliseconds: 1000, defaultCachingStore: \"local\", sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/\", \"id\", \"secret\"); }, headers: { \"X-MyRequiredHeader\": \"SomeValue\", \"X-MyRequiredHeader2\": \"SomeValue\", }, }, }); const isolatedSP = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"https://mytenant.sharepoint.com/site/dev\", \"id\", \"secret\"); }, }, }, });","title":"Node multiple site requests"},{"location":"v2/concepts/configuration/#batching","text":"All batching functionality works as expected, but you must take care to only associate requests from the same isolated instance as you create the batch. Mixing requests across isolation boundaries is not supported. This applies to sp and graph batching. sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"url1\", \"id\", \"secret\"); }, }, }); const isolated = await sp.createIsolated({ config: { sp: { fetchClientFactory: () => { return new SPFetchClient(\"url2\", \"id\", \"secret\"); }, }, }, }); const batch1 = sp.createBatch(); sp.web.lists.select(\"Title\").top(3).inBatch(batch1)().then(r => console.log(`here 1: ${JSON.stringify(r, null, 2)}`)); sp.web.select(\"Title\").inBatch(batch1)().then(r => console.log(`here 2: ${JSON.stringify(r, null, 2)}`)); await batch1.execute(); const batch2 = isolated.createBatch(); isolated.web.lists.select(\"Title\").top(3).inBatch(batch2)().then(r => console.log(`here 3: ${JSON.stringify(r, null, 2)}`)); isolated.web.select(\"Title\").inBatch(batch2)().then(r => console.log(`here 4: ${JSON.stringify(r, null, 2)}`)); await batch2.execute();","title":"Batching"},{"location":"v2/concepts/configuration/#ie11-mode","text":"The IE11 mode setting is always global. There is no scenario we care to support where once instance needs to run in ie11 mode and another does not. Your code either does or does not run in ie11.","title":"IE11 Mode"},{"location":"v2/concepts/configuration/#prior-to-v210","text":"","title":"Prior to v2.1.0"},{"location":"v2/concepts/configuration/#architecture_1","text":"PnPjs uses an additive configuration design with multiple libraries sharing a single global configuration instance. If you need non-global configuration please see this section . There are three ways to access the setup functionality - through either the common, sp, or graph library's setup method. While the configuration is global the various methods have different typing on their input parameter. You can review the libconfig article for more details on storing your own configuration.","title":"Architecture"},{"location":"v2/concepts/configuration/#common-configuration","text":"The common libary's setup method takes parameters defined by ILibraryConfiguration . The properties and their defaults are listed below, followed by a code sample. You can call setup multiple times and any new values will be added to the existing configuration or replace the previous value if one existed. All values are optional. Name Description Default defaultCachingStore Where will PnPjs store cached data by default (session or local) session defaultCachingTimeoutSeconds The global default value used for cached data timeouts in seconds 60 globalCacheDisable Provides a way to globally within PnPjs disable all caching false enableCacheExpiration If true a timeout expired items will be removed from the cache in intervals determined by cacheTimeoutInterval false cacheExpirationIntervalMilliseconds Determines the interval in milliseconds at which the cache is checked to see if items have expired (min: 100) 750 spfxContext When running in SPFx the current context should always be supplied to PnPjs when available null ie11 If true the library downgrades functionality to work in IE11 false For more information on setting up in SPFx please see the authentication section For more details on ie11 mode please see the topic article import { setup } from \"@pnp/core\"; // called before other code setup({ cacheExpirationIntervalMilliseconds: 15000, defaultCachingStore: \"local\", defaultCachingTimeoutSeconds: 600, enableCacheExpiration: true, globalCacheDisable: false, ie11: false, spfxContext: this.context, // if in SPFx, otherwise leave it out });","title":"Common Configuration"},{"location":"v2/concepts/configuration/#sp-configuration","text":"The sp library's configuration is defined by the ISPConfiguration interface which extends ILibraryConfiguration. All of the sp values are contained in a top level property named \"sp\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { sp } from \"@pnp/sp\"; import { SPFxAdalClient } from \"@pnp/core\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration sp.setup({ ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", fetchClientFactory: () => { return new SPFxAdalClient(this.context); }, headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, });","title":"SP Configuration"},{"location":"v2/concepts/configuration/#sharepoint-framework","text":"You can optionally supply only the SPFx context to the sp configure method. import { sp } from \"@pnp/sp\"; // in SPFx only sp.setup(this.context);","title":"SharePoint Framework"},{"location":"v2/concepts/configuration/#graph-configuration","text":"The graph configuration works exactly the same as the sp configuration but is defined by the IGraphConfiguration interface which extends ILibraryConfiguration. All of the graph values are contained in a top level property named \"graph\". The following table describes the properties with a code sample following. All values are optional. Name Description Default headers Allows you to apply any headers to all calls made by the sp library none baseUrl Allows you to define a base site url for all requests, takes precedence over all other url logic. Must be absolute. ( Added in 2.0.8 ) none fetchClientFactory Allows you to specify a factory function used to produce IHttpClientImpl instances none There are many examples of using fetchClientFactory available in the authentication section . import { graph } from \"@pnp/graph\"; import { MsalClientSetup } from \"@pnp/msaljsclient\"; // note you can still set the global configuration such as ie11 using the same object as // the interface extends ILibraryConfiguration graph.setup({ ie11: false, graph: { // we set the GCC url baseUrl: \"https://graph.microsoft.us\", fetchClientFactory: MsalClientSetup({ auth: { authority: \"https://login.microsoftonline.com/tenant.onmicrosoft.com\", clientId: \"00000000-0000-0000-0000-000000000000\", redirectUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/test.aspx\", }, }, [\"Group.Read.All\"]), headers: { \"Accept\": \"application/json;odata=verbose\", \"X-Something\": \"header-value\", }, }, spfxContext: this.context, });","title":"Graph Configuration"},{"location":"v2/concepts/configuration/#sharepoint-framework_1","text":"You can optionally supply only the SPFx context to the graph configure method. We will attempt to set the baseUrl property from the context - but if that is failing in your environment and you need to call a special cloud (i.e. graph.microsoft.us) please set the baseUrl property. import { graph } from \"@pnp/graph\"; // in SPFx only graph.setup(this.context);","title":"SharePoint Framework"},{"location":"v2/concepts/configuration/#configure-everything-at-once","text":"In some cases you might want to configure everything in one go. Because the configuration is stored in a single location you can use the common library's setup method and adjust the typings to ensure you are using the correct property names while only having to setup things with a single call. In versions before 2.0.8 ISPConfigurationPart, IGraphConfigurationPart, and ILibraryConfiguration incorrectly were missing the \"I\" prefix. That was fixed in 2.0.8 - but note if you are using an older version of the library you'll need to use the old names. Everything else in the below example works as expected. import { ISPConfigurationPart } from \"@pnp/sp\"; import { IGraphConfigurationPart } from \"@pnp/graph\"; import { ILibraryConfiguration, setup } from \"@pnp/core\"; // you could also include your custom configuration parts export interface AllConfig extends ILibraryConfiguration, ISPConfigurationPart, IGraphConfigurationPart { } // create a single big configuration entry const config: AllConfig = { graph: { baseUrl: \"https://graph.microsoft.us\", }, ie11: false, sp: { baseUrl: \"https://tenant.sharepoint.com/sites/dev\", }, }; setup(config);","title":"Configure Everything At Once"},{"location":"v2/concepts/custom-bundle/","text":"Custom Bundling \u00b6 With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles. Scenarios could include: Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once. Creating SPFx libraries either for one project or a single webpart. Create a single library containing the PnPjs code you need bundled along with your custom extensions . Create a custom bundle \u00b6 Webpack \u00b6 You can see/clone a sample project of this example here . Rollup \u00b6 You can see/clone a sample project of this example here .","title":"Custom Bundling"},{"location":"v2/concepts/custom-bundle/#custom-bundling","text":"With the introduction of selective imports it is now possible to create your own bundle to exactly fit your needs. This provides much greater control over how your solutions are deployed and what is included in your bundles. Scenarios could include: Deploying a company-wide PnPjs custom bundle shared by all your components so it only needs to be downloaded once. Creating SPFx libraries either for one project or a single webpart. Create a single library containing the PnPjs code you need bundled along with your custom extensions .","title":"Custom Bundling"},{"location":"v2/concepts/custom-bundle/#create-a-custom-bundle","text":"","title":"Create a custom bundle"},{"location":"v2/concepts/custom-bundle/#webpack","text":"You can see/clone a sample project of this example here .","title":"Webpack"},{"location":"v2/concepts/custom-bundle/#rollup","text":"You can see/clone a sample project of this example here .","title":"Rollup"},{"location":"v2/concepts/error-handling/","text":"Error Handling \u00b6 This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns. For 429, 503, and 504 errors we include retry logic within the library The HttpRequestError \u00b6 All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error . In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible. Property Name Description name Standard Error.name property. Always 'Error' message Normalized string containing the status, status text, and the full response text stack The callstack producing the error isHttpRequestError Always true, allows you to reliably determine if you have an HttpRequestError instance response Unread copy of the Response object resulting in the thrown error status The Response.status value (such as 404) statusText The Response.statusText value (such as 'Not Found') Basic Handling \u00b6 For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen \ud83d\ude09. The most basic type of error handling involves a simple try-catch. import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { console.error(e); } This will produce output like: Error making HttpClient request in queryable [404] Not Found ::> {\"odata.error\":{\"code\":\"-1, System.ArgumentException\",\"message\":{\"lang\":\"en-US\",\"value\":\"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'.\"}}} Data: {\"response\":{\"size\":0,\"timeout\":0},\"status\":404,\"statusText\":\"Not Found\",\"isHttpRequestError\":true} This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly. Reading the Response \u00b6 In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire: import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { // are we dealing with an HttpRequestError? if (e?.isHttpRequestError) { // we can read the json from the response const json = await (e).response.json(); // if we have a value property we can show it console.log(typeof json[\"odata.error\"] === \"object\" ? json[\"odata.error\"].message.value : e.message); // add of course you have access to the other properties and can make choices on how to act if ((e).status === 404) { console.error((e).statusText); // maybe create the resource, or redirect, or fallback to a secondary data source // just ideas, handle any of the status codes uniquely as needed } } else { // not an HttpRequestError so we just log message console.log(e.message); } } Logging errors \u00b6 Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework. import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { Logger.error(e); } You may want to read the response and customize the message as described above: import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { if (e?.isHttpRequestError) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } } Putting it All Together \u00b6 After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application. errorhandler.ts \u00b6 import { Logger } from \"@pnp/logging\"; import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { if (hOP(e, \"isHttpRequestError\")) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } } web-request.ts \u00b6 import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { await handleError(e); } web-request2.ts \u00b6 import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists(); } catch (e) { await handleError(e); }","title":"Error Handling"},{"location":"v2/concepts/error-handling/#error-handling","text":"This article describes the most common types of errors generated by the library. It provides context on the error object, and ways to handle the errors. As always you should tailor your error handling to what your application needs. These are ideas that can be applied to many different patterns. For 429, 503, and 504 errors we include retry logic within the library","title":"Error Handling"},{"location":"v2/concepts/error-handling/#the-httprequesterror","text":"All errors resulting from executed web requests will be returned as an HttpRequestError object which extends the base Error . In addition to the standard Error properties it has some other properties to help you figure out what went wrong. We used a custom error to attempt to normalize what can be a wide assortment of http related errors, while also seeking to provide as much information to library consumers as possible. Property Name Description name Standard Error.name property. Always 'Error' message Normalized string containing the status, status text, and the full response text stack The callstack producing the error isHttpRequestError Always true, allows you to reliably determine if you have an HttpRequestError instance response Unread copy of the Response object resulting in the thrown error status The Response.status value (such as 404) statusText The Response.statusText value (such as 'Not Found')","title":"The HttpRequestError"},{"location":"v2/concepts/error-handling/#basic-handling","text":"For all operations involving a web request you should account for the possibility they might fail. That failure might be transient or permanent - you won't know until they happen \ud83d\ude09. The most basic type of error handling involves a simple try-catch. import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { console.error(e); } This will produce output like: Error making HttpClient request in queryable [404] Not Found ::> {\"odata.error\":{\"code\":\"-1, System.ArgumentException\",\"message\":{\"lang\":\"en-US\",\"value\":\"List 'no' does not exist at site with URL 'https://tenant.sharepoint.com/sites/dev'.\"}}} Data: {\"response\":{\"size\":0,\"timeout\":0},\"status\":404,\"statusText\":\"Not Found\",\"isHttpRequestError\":true} This is very descriptive and provides full details as to what happened, but you might want to handle things a little more cleanly.","title":"Basic Handling"},{"location":"v2/concepts/error-handling/#reading-the-response","text":"In some cases the response body will have additional details such as a localized error messages which can be nicer to display rather than our normalized string. You can read the response directly and process it however you desire: import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { // are we dealing with an HttpRequestError? if (e?.isHttpRequestError) { // we can read the json from the response const json = await (e).response.json(); // if we have a value property we can show it console.log(typeof json[\"odata.error\"] === \"object\" ? json[\"odata.error\"].message.value : e.message); // add of course you have access to the other properties and can make choices on how to act if ((e).status === 404) { console.error((e).statusText); // maybe create the resource, or redirect, or fallback to a secondary data source // just ideas, handle any of the status codes uniquely as needed } } else { // not an HttpRequestError so we just log message console.log(e.message); } }","title":"Reading the Response"},{"location":"v2/concepts/error-handling/#logging-errors","text":"Using the PnPjs Logging Framework you can directly pass the error object and the normalized message will be logged. These techniques can be applied to any logging framework. import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { Logger.error(e); } You may want to read the response and customize the message as described above: import { Logger } from \"@pnp/logging\"; import { sp } from \"@pnp/sp/presets/all\"; import { HttpRequestError } from \"@pnp/queryable\"; try { // get a list that doesn't exist const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { if (e?.isHttpRequestError) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } }","title":"Logging errors"},{"location":"v2/concepts/error-handling/#putting-it-all-together","text":"After reviewing the above section you might have thought it seems like a lot of work to include all that logic for every error. One approach is to establish a single function you use application wide to process errors. This allows all the error handling logic to be easily updated and consistent across the application.","title":"Putting it All Together"},{"location":"v2/concepts/error-handling/#errorhandlerts","text":"import { Logger } from \"@pnp/logging\"; import { HttpRequestError } from \"@pnp/queryable\"; import { hOP } from \"@pnp/core\"; export async function handleError(e: Error | HttpRequestError): Promise { if (hOP(e, \"isHttpRequestError\")) { // we can read the json from the response const data = await (e).response.json(); // parse this however you want const message = typeof data[\"odata.error\"] === \"object\" ? data[\"odata.error\"].message.value : e.message; // we use the status to determine a custom logging level const level: LogLevel = (e).status === 404 ? LogLevel.Warning : LogLevel.Info; // create a custom log entry Logger.log({ data, level, message, }); } else { // not an HttpRequestError so we just log message Logger.error(e); } }","title":"errorhandler.ts"},{"location":"v2/concepts/error-handling/#web-requestts","text":"import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists.getByTitle(\"no\")(); } catch (e) { await handleError(e); }","title":"web-request.ts"},{"location":"v2/concepts/error-handling/#web-request2ts","text":"import { sp } from \"@pnp/sp/presets/all\"; import { handleError } from \"./errorhandler\"; try { const w = await sp.web.lists(); } catch (e) { await handleError(e); }","title":"web-request2.ts"},{"location":"v2/concepts/ie11-mode/","text":"IE11 Mode \u00b6 Starting with v2 we have made the decision to no longer support IE11. Because we know this affects folks we have introduced IE11 compatibility mode. Configuring the library will allow it to work within IE11, however at a possibly reduced level of functionality depending on your use case. Please see the list below of known limitations. Limitations \u00b6 When required to use IE11 mode there is certain functionality which may not work correctly or at all. Unavailable: Extension Methods Unavailable: OData Debugging Configure IE11 Mode \u00b6 To enable IE11 Mode set the ie11 flag to true in the setup object. Optionally, supply the context object when working in SharePoint Framework . import { sp } from \"@pnp/sp\"; sp.setup({ // set ie 11 mode ie11: true, // only needed when working within SharePoint Framework spfxContext: this.context }); If you are supporting IE 11, please see the article on required polyfills . A note on ie11 mode and support \u00b6 Because IE11 is no longer a primary supported browser our policy moving forward will be doing our best not to break anything in ie11 mode, but not all features will work and new features may never come to ie11 mode. Also, if you find an ie11 bug we expect you to work with us on helping to fix it. If you aren't willing to invest some time to support an old browser it seems we shouldn't either.","title":"IE11 Mode"},{"location":"v2/concepts/ie11-mode/#ie11-mode","text":"Starting with v2 we have made the decision to no longer support IE11. Because we know this affects folks we have introduced IE11 compatibility mode. Configuring the library will allow it to work within IE11, however at a possibly reduced level of functionality depending on your use case. Please see the list below of known limitations.","title":"IE11 Mode"},{"location":"v2/concepts/ie11-mode/#limitations","text":"When required to use IE11 mode there is certain functionality which may not work correctly or at all. Unavailable: Extension Methods Unavailable: OData Debugging","title":"Limitations"},{"location":"v2/concepts/ie11-mode/#configure-ie11-mode","text":"To enable IE11 Mode set the ie11 flag to true in the setup object. Optionally, supply the context object when working in SharePoint Framework . import { sp } from \"@pnp/sp\"; sp.setup({ // set ie 11 mode ie11: true, // only needed when working within SharePoint Framework spfxContext: this.context }); If you are supporting IE 11, please see the article on required polyfills .","title":"Configure IE11 Mode"},{"location":"v2/concepts/ie11-mode/#a-note-on-ie11-mode-and-support","text":"Because IE11 is no longer a primary supported browser our policy moving forward will be doing our best not to break anything in ie11 mode, but not all features will work and new features may never come to ie11 mode. Also, if you find an ie11 bug we expect you to work with us on helping to fix it. If you aren't willing to invest some time to support an old browser it seems we shouldn't either.","title":"A note on ie11 mode and support"},{"location":"v2/concepts/invokable/","text":"Invokables \u00b6 For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain: // an example of get const lists = await sp.web.lists(); Starting with v2 this is no longer required, you can invoke the object directly to execute the default action for that class - typically a get. const lists = await sp.web.lists(); This has two main benefits for people using the library: you can write less code, and we now have a way to model default actions for objects that might do something other than a get. The way we designed the library prior to v2 hid the post, put, delete operations as protected methods attached to the Queryable classes. Without diving into why we did this, having a rethink seemed appropriate for v2. Based on that, the entire queryable chain is now invokable as well for any of the operations. Other Operations (post, put, delete) \u00b6 import { sp, spPost } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // do a post to a web - just an example doesn't do anything fancy spPost(sp.web); Things get a little more interesting in that you can now do posts (or any of the operations) to any of the urls defined by a fluent chain. Meaning you can easily implement methods that are not yet part of the library. For this example I have made up a method called \"MagicFieldCreationMethod\" that doesn't exist. Imagine it was just added to the SharePoint API and we do not yet have support for it. You can now write code like so: import { sp, spPost, SharePointQueryable } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields/web\"; // call our made up example method spPost(SharePointQueryable(sp.web.fields, \"MagicFieldCreationMethod\"), { body: JSON.stringify({ // ... this would be the post body }), });","title":"Invokables"},{"location":"v2/concepts/invokable/#invokables","text":"For people who have been using the library since the early days you are familiar with the need to use the () method to invoke a method chain: // an example of get const lists = await sp.web.lists(); Starting with v2 this is no longer required, you can invoke the object directly to execute the default action for that class - typically a get. const lists = await sp.web.lists(); This has two main benefits for people using the library: you can write less code, and we now have a way to model default actions for objects that might do something other than a get. The way we designed the library prior to v2 hid the post, put, delete operations as protected methods attached to the Queryable classes. Without diving into why we did this, having a rethink seemed appropriate for v2. Based on that, the entire queryable chain is now invokable as well for any of the operations.","title":"Invokables"},{"location":"v2/concepts/invokable/#other-operations-post-put-delete","text":"import { sp, spPost } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // do a post to a web - just an example doesn't do anything fancy spPost(sp.web); Things get a little more interesting in that you can now do posts (or any of the operations) to any of the urls defined by a fluent chain. Meaning you can easily implement methods that are not yet part of the library. For this example I have made up a method called \"MagicFieldCreationMethod\" that doesn't exist. Imagine it was just added to the SharePoint API and we do not yet have support for it. You can now write code like so: import { sp, spPost, SharePointQueryable } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields/web\"; // call our made up example method spPost(SharePointQueryable(sp.web.fields, \"MagicFieldCreationMethod\"), { body: JSON.stringify({ // ... this would be the post body }), });","title":"Other Operations (post, put, delete)"},{"location":"v2/concepts/polyfill/","text":"Polyfills \u00b6 These libraries may make use of some features not found in older browsers. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. If you are supporting IE11 enable IE11 mode . IE 11 Polyfill package \u00b6 We created a package you try and help provide this missing functionality. This package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you are required to support IE 11. Install \u00b6 npm install @pnp/polyfill-ie11 --save Use \u00b6 import \"@pnp/polyfill-ie11\"; import { sp } from \"@pnp/sp/presets/all\"; sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); }); Selective Use \u00b6 Starting with version 2.0.2 you can selectively include the polyfills from the package. Depending on your needs it may make sense in your application to use the underlying libraries directly. We have added an expanded statement on our polyfills . // individually include polyfills as needed to match your requirements import \"@pnp/polyfill-ie11/dist/fetch\"; import \"@pnp/polyfill-ie11/dist/fill\"; import \"@pnp/polyfill-ie11/dist/from\"; import \"@pnp/polyfill-ie11/dist/iterator\"; import \"@pnp/polyfill-ie11/dist/map\"; import \"@pnp/polyfill-ie11/dist/promise\"; import \"@pnp/polyfill-ie11/dist/reflect\"; import \"@pnp/polyfill-ie11/dist/symbol\"; // works in IE11 and other browsers sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); }); SearchQueryBuilder \u00b6 Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version as shown below. import \"@pnp/polyfill-ie11\"; import { SearchQueryBuilder } from \"@pnp/polyfill-ie11/dist/searchquerybuilder\"; import { sp, ISearchQueryBuilder } from \"@pnp/sp/presets/all\"; // works in IE11 and other browsers const builder: ISearchQueryBuilder = SearchQueryBuilder().text(\"test\"); sp.search(builder).then(r => { this.domElement.innerHTML = JSON.stringify(r); }); General Statement on Polyfills \u00b6 Internet Explorer 11 (IE11) has been an enterprise standard browser for many years. Given the complexity in changing technical platforms in many organizations, it is no surprise standardization on this out-of-date browser continues. Unfortunately, for those organizations, the Internet has moved on and many - if not all - SaaS platforms are embracing modern standards and no longer supporting the legacy IE11 browser. Even Microsoft states in their official documentation that Microsoft 365 is best experienced with a modern browser. They have even gone so far to build the latest version of Microsoft Edge based on Chromium (Edge Chromium), with an \"Internet Explorer mode\" allowing organizations to load legacy sites which require IE automatically. PnPjs is now \"modern\" as well, and by that we mean we have moved to using capabilities of current browsers and JavaScript which are not present in IE11. We understand as a developer your ability to require an organization to switch browsers is unrealistic. We want to do everything we can to support you, but it is up to you to ensure your application is properly supported in IE11. There are many polyfills available, depending on the platform you're running on, the frameworks you are using, and the libraries you consume. Although the majority of PnPjs users build for SharePoint Online, a significant number build for earlier versions of the platform as well as for their own node-based solutions or websites. Unfortunately, there is no way our polyfill library can support all these scenarios. What we intended with the @pnp/polyfill-ie11 package was to provide a comprehensive group of all the polyfills that would be needed based on the complete PnPjs library. We are finding when we aggregate our polyfills with the polyfills provided in the SharePoint page and from other sources, things don't always work well. We cannot solve this for your specific situations except by providing you transparency into the polyfills which we know are necessary for our packages. You may need to adjust what polyfills your application uses based on the other libraries you are using. To that end, we want to provide the list of polyfills we recommend here - along with the associated packages \u2013 with the goal of helping you to work out what combination of polyfills might work with your code. Also, if you haven't reviewed it yet, please check out the information on IE11 Mode for how to configure IE11 mode in the sp.setup as well as what limitations doing so will have on your usage of PnPjs. imports import \"core-js/stable/array/from\"; import \"core-js/stable/array/fill\"; import \"core-js/stable/array/iterator\"; import \"core-js/stable/promise\"; import \"core-js/stable/reflect\"; import \"es6-map/implement\"; import \"core-js/stable/symbol\"; import \"whatwg-fetch\"; The following NPM packages are what we use to do the above indicated imports |package| |---| | core-js | | es6-map | | whatwg-fetch |","title":"Polyfills"},{"location":"v2/concepts/polyfill/#polyfills","text":"These libraries may make use of some features not found in older browsers. This primarily affects Internet Explorer 11, which requires that we provide this missing functionality. If you are supporting IE11 enable IE11 mode .","title":"Polyfills"},{"location":"v2/concepts/polyfill/#ie-11-polyfill-package","text":"We created a package you try and help provide this missing functionality. This package is independent of the other @pnp/* packages and does not need to be updated monthly unless we introduce additional polyfills and publish a new version. This package is only needed if you are required to support IE 11.","title":"IE 11 Polyfill package"},{"location":"v2/concepts/polyfill/#install","text":"npm install @pnp/polyfill-ie11 --save","title":"Install"},{"location":"v2/concepts/polyfill/#use","text":"import \"@pnp/polyfill-ie11\"; import { sp } from \"@pnp/sp/presets/all\"; sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); });","title":"Use"},{"location":"v2/concepts/polyfill/#selective-use","text":"Starting with version 2.0.2 you can selectively include the polyfills from the package. Depending on your needs it may make sense in your application to use the underlying libraries directly. We have added an expanded statement on our polyfills . // individually include polyfills as needed to match your requirements import \"@pnp/polyfill-ie11/dist/fetch\"; import \"@pnp/polyfill-ie11/dist/fill\"; import \"@pnp/polyfill-ie11/dist/from\"; import \"@pnp/polyfill-ie11/dist/iterator\"; import \"@pnp/polyfill-ie11/dist/map\"; import \"@pnp/polyfill-ie11/dist/promise\"; import \"@pnp/polyfill-ie11/dist/reflect\"; import \"@pnp/polyfill-ie11/dist/symbol\"; // works in IE11 and other browsers sp.web.lists.getByTitle(\"BigList\").items.filter(`ID gt 6000`)().then(r => { this.domElement.innerHTML += r.map(l => `${l.Title}
    `); });","title":"Selective Use"},{"location":"v2/concepts/polyfill/#searchquerybuilder","text":"Because the latest version of SearchQueryBuilder uses Proxy internally you can fall back on the older version as shown below. import \"@pnp/polyfill-ie11\"; import { SearchQueryBuilder } from \"@pnp/polyfill-ie11/dist/searchquerybuilder\"; import { sp, ISearchQueryBuilder } from \"@pnp/sp/presets/all\"; // works in IE11 and other browsers const builder: ISearchQueryBuilder = SearchQueryBuilder().text(\"test\"); sp.search(builder).then(r => { this.domElement.innerHTML = JSON.stringify(r); });","title":"SearchQueryBuilder"},{"location":"v2/concepts/polyfill/#general-statement-on-polyfills","text":"Internet Explorer 11 (IE11) has been an enterprise standard browser for many years. Given the complexity in changing technical platforms in many organizations, it is no surprise standardization on this out-of-date browser continues. Unfortunately, for those organizations, the Internet has moved on and many - if not all - SaaS platforms are embracing modern standards and no longer supporting the legacy IE11 browser. Even Microsoft states in their official documentation that Microsoft 365 is best experienced with a modern browser. They have even gone so far to build the latest version of Microsoft Edge based on Chromium (Edge Chromium), with an \"Internet Explorer mode\" allowing organizations to load legacy sites which require IE automatically. PnPjs is now \"modern\" as well, and by that we mean we have moved to using capabilities of current browsers and JavaScript which are not present in IE11. We understand as a developer your ability to require an organization to switch browsers is unrealistic. We want to do everything we can to support you, but it is up to you to ensure your application is properly supported in IE11. There are many polyfills available, depending on the platform you're running on, the frameworks you are using, and the libraries you consume. Although the majority of PnPjs users build for SharePoint Online, a significant number build for earlier versions of the platform as well as for their own node-based solutions or websites. Unfortunately, there is no way our polyfill library can support all these scenarios. What we intended with the @pnp/polyfill-ie11 package was to provide a comprehensive group of all the polyfills that would be needed based on the complete PnPjs library. We are finding when we aggregate our polyfills with the polyfills provided in the SharePoint page and from other sources, things don't always work well. We cannot solve this for your specific situations except by providing you transparency into the polyfills which we know are necessary for our packages. You may need to adjust what polyfills your application uses based on the other libraries you are using. To that end, we want to provide the list of polyfills we recommend here - along with the associated packages \u2013 with the goal of helping you to work out what combination of polyfills might work with your code. Also, if you haven't reviewed it yet, please check out the information on IE11 Mode for how to configure IE11 mode in the sp.setup as well as what limitations doing so will have on your usage of PnPjs. imports import \"core-js/stable/array/from\"; import \"core-js/stable/array/fill\"; import \"core-js/stable/array/iterator\"; import \"core-js/stable/promise\"; import \"core-js/stable/reflect\"; import \"es6-map/implement\"; import \"core-js/stable/symbol\"; import \"whatwg-fetch\"; The following NPM packages are what we use to do the above indicated imports |package| |---| | core-js | | es6-map | | whatwg-fetch |","title":"General Statement on Polyfills"},{"location":"v2/concepts/selective-imports/","text":"Selective Imports \u00b6 As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports in v2. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking . This concept works well with custom bundling to create a shared package tailored exactly to your needs. If you would prefer to not worry about selective imports please see the section on presets . Old way \u00b6 // the sp var came with all library functionality already attached // meaning treeshaking couldn't reduce the size import { sp } from \"@pnp/sp\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); New Way \u00b6 // the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific // the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example: // this import statement will attach content-type functionality to list, web, and item import \"@pnp/sp/content-types\"; // this import statement will only attach content-type functionality to web import \"@pnp/sp/content-types/web\"; If you only need to access content types on the web object you can reduce size by only importing that piece. // this will fail import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IList } from \"@pnp/sp/lists\"; // do this instead import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { IList } from \"@pnp/sp/lists\"; const lists = await sp.web.lists(); Presets \u00b6 Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually. SP \u00b6 For the sp library there are two presets \"all\" and \"core\". The all preset mimics the behavior in v1 and includes everything in the library already attached to the sp var. import { sp } from \"@pnp/sp/presets/all\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists(); The \"core\" preset includes sites, webs, lists, and items. import { sp } from \"@pnp/sp/presets/core\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists(); Graph \u00b6 The graph library contains a single preset, \"all\" mimicking the v1 structure. import { graph } from \"@pnp/graph/presets/all\"; // graph.* exists as it did in v1, tree shaking will not work While we may look to add additional presets in the future you are encouraged to look at making your own custom bundles as a preferred solution.","title":"Selective Imports"},{"location":"v2/concepts/selective-imports/#selective-imports","text":"As the libraries have grown to support more of the SharePoint and Graph API they have also grown in size. On one hand this is good as more functionality becomes available but you had to include lots of code you didn't use if you were only doing simple operations. To solve this we introduced selective imports in v2. This allows you to only import the parts of the sp or graph library you need, allowing you to greatly reduce your overall solution bundle size - and enables treeshaking . This concept works well with custom bundling to create a shared package tailored exactly to your needs. If you would prefer to not worry about selective imports please see the section on presets .","title":"Selective Imports"},{"location":"v2/concepts/selective-imports/#old-way","text":"// the sp var came with all library functionality already attached // meaning treeshaking couldn't reduce the size import { sp } from \"@pnp/sp\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)();","title":"Old way"},{"location":"v2/concepts/selective-imports/#new-way","text":"// the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); Above we are being very specific in what we are importing, but you can also import entire sub-modules and be slightly less specific // the sp var now has almost nothing attached at import time and relies on import { sp } from \"@pnp/sp\"; // we need to import each of the pieces we need to \"attach\" them for chaining // here we are importing the specific sub modules we need and attaching the functionality for lists to web and items to list import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const itemData = await sp.web.lists.getById('00000000-0000-0000-0000-000000000000').items.getById(1)(); The above two examples both work just fine but you may end up with slightly smaller bundle sizes using the first. Consider this example: // this import statement will attach content-type functionality to list, web, and item import \"@pnp/sp/content-types\"; // this import statement will only attach content-type functionality to web import \"@pnp/sp/content-types/web\"; If you only need to access content types on the web object you can reduce size by only importing that piece. // this will fail import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IList } from \"@pnp/sp/lists\"; // do this instead import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { IList } from \"@pnp/sp/lists\"; const lists = await sp.web.lists();","title":"New Way"},{"location":"v2/concepts/selective-imports/#presets","text":"Sometimes you don't care as much about bundle size - testing or node development for example. In these cases we have provided what we are calling presets to allow you to skip importing each module individually.","title":"Presets"},{"location":"v2/concepts/selective-imports/#sp","text":"For the sp library there are two presets \"all\" and \"core\". The all preset mimics the behavior in v1 and includes everything in the library already attached to the sp var. import { sp } from \"@pnp/sp/presets/all\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists(); The \"core\" preset includes sites, webs, lists, and items. import { sp } from \"@pnp/sp/presets/core\"; // sp.* exists as it did in v1, tree shaking will not work const lists = await sp.web.lists();","title":"SP"},{"location":"v2/concepts/selective-imports/#graph","text":"The graph library contains a single preset, \"all\" mimicking the v1 structure. import { graph } from \"@pnp/graph/presets/all\"; // graph.* exists as it did in v1, tree shaking will not work While we may look to add additional presets in the future you are encouraged to look at making your own custom bundles as a preferred solution.","title":"Graph"},{"location":"v2/concepts/settings/","text":"Project Settings \u00b6 This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally. The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root . Settings File Format (>= 2.0.13) \u00b6 Starting with version 2.0.13 we have added support within the settings file for MSAL authentication for both SharePoint and Graph. You are NOT required to update your existing settings file unless you want to use MSAL authentication with a Graph application. The existing id/secret settings continue to work however we recommend updating when you have an opportunity. For more information coinfiguring MSAL please review the section in the authentication section for node . MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always \"https://{tenant}.sharepoint.com/.default\" or \"https://graph.microsoft.com/.default\" depending on what you are calling. If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated. const privateKey = `-----BEGIN RSA PRIVATE KEY----- your private key, read from a file or included here -----END RSA PRIVATE KEY----- `; var msalInit = { auth: { authority: \"https://login.microsoftonline.com/{tenant id}\", clientCertificate: { thumbprint: \"{certificate thumbnail}\", privateKey: privateKey, }, clientId: \"{AAD App registration id}\", } } var settings = { testing: { enableWebTests: true, testUser: \"i:0#.f|membership|user@consto.com\", sp: { url: \"{required for MSAL - absolute url of test site}\", notificationUrl: \"{ optional: notification url }\", msal: { init: msalInit, scopes: [\"https://{tenant}.sharepoint.com/.default\"] }, }, graph: { msal: { init: msalInit, scopes: [\"https://graph.microsoft.com/.default\"] }, }, }, } module.exports = settings; The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below. enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. testUser AAD login account to be used when running tests. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests SP values \u00b6 name description url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions msal Information about MSAL authentication setup Graph value \u00b6 The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description msal Information about MSAL authentication setup Settings File Format (<= 2.0.12) \u00b6 var settings = { testing: { enableWebTests: true, sp: { id: \"{ client id }\", secret: \"{ client secret }\", url: \"{ site collection url }\", notificationUrl: \"{ optional: notification url }\", }, graph: { tenant: \"{tenant.onmicrosoft.com}\", id: \"{your app id}\", secret: \"{your secret}\" }, } } module.exports = settings; enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests SP values \u00b6 The sp values are described in the table below and come from registering a legacy SharePoint add-in . name description id The client id of the registered application secret The client secret of the registered application url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions Graph values \u00b6 The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description tenant Tenant to target for authentication and data (ex: contoso.onmicrosoft.com) id The application id secret The application secret Create Settings.js file \u00b6 Copy the example file and rename it settings.js. Place the file in the root of your project. Update the settings as needed for your environment. If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.","title":"Project Settings"},{"location":"v2/concepts/settings/#project-settings","text":"This article discusses creating a project settings file for use in local development and debugging of the libraries. The settings file contains authentication and other settings to enable you to run and debug the project locally. The settings file is a JavaScript file that exports a single object representing the settings of your project. You can view the example settings file in the project root .","title":"Project Settings"},{"location":"v2/concepts/settings/#settings-file-format-2013","text":"Starting with version 2.0.13 we have added support within the settings file for MSAL authentication for both SharePoint and Graph. You are NOT required to update your existing settings file unless you want to use MSAL authentication with a Graph application. The existing id/secret settings continue to work however we recommend updating when you have an opportunity. For more information coinfiguring MSAL please review the section in the authentication section for node . MSAL configuration has two parts, these are the initialization which is passed directly to the MsalFetchClient (and on to the underlying msal-node instance) and the scopes. The scopes are always \"https://{tenant}.sharepoint.com/.default\" or \"https://graph.microsoft.com/.default\" depending on what you are calling. If you are calling Microsoft Graph sovereign or gov clouds the scope may need to be updated. const privateKey = `-----BEGIN RSA PRIVATE KEY----- your private key, read from a file or included here -----END RSA PRIVATE KEY----- `; var msalInit = { auth: { authority: \"https://login.microsoftonline.com/{tenant id}\", clientCertificate: { thumbprint: \"{certificate thumbnail}\", privateKey: privateKey, }, clientId: \"{AAD App registration id}\", } } var settings = { testing: { enableWebTests: true, testUser: \"i:0#.f|membership|user@consto.com\", sp: { url: \"{required for MSAL - absolute url of test site}\", notificationUrl: \"{ optional: notification url }\", msal: { init: msalInit, scopes: [\"https://{tenant}.sharepoint.com/.default\"] }, }, graph: { msal: { init: msalInit, scopes: [\"https://graph.microsoft.com/.default\"] }, }, }, } module.exports = settings; The settings object has a single sub-object testing which contains the configuration used for debugging and testing PnPjs. The parts of this object are described in detail below. enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. testUser AAD login account to be used when running tests. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests","title":"Settings File Format (>= 2.0.13)"},{"location":"v2/concepts/settings/#sp-values","text":"name description url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions msal Information about MSAL authentication setup","title":"SP values"},{"location":"v2/concepts/settings/#graph-value","text":"The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description msal Information about MSAL authentication setup","title":"Graph value"},{"location":"v2/concepts/settings/#settings-file-format-2012","text":"var settings = { testing: { enableWebTests: true, sp: { id: \"{ client id }\", secret: \"{ client secret }\", url: \"{ site collection url }\", notificationUrl: \"{ optional: notification url }\", }, graph: { tenant: \"{tenant.onmicrosoft.com}\", id: \"{your app id}\", secret: \"{your secret}\" }, } } module.exports = settings; enableWebTests Flag to toggle if tests are run against the live services or not. If this is set to false none of the other sections are required. sp Settings used to configure SharePoint (sp library) debugging and tests graph Settings used to configure Microsoft Graph (graph library) debugging and tests","title":"Settings File Format (<= 2.0.12)"},{"location":"v2/concepts/settings/#sp-values_1","text":"The sp values are described in the table below and come from registering a legacy SharePoint add-in . name description id The client id of the registered application secret The client secret of the registered application url The url of the site to use for all requests. If a site parameter is not specified a child web will be created under the web at this url. See scripts article for more details. notificationUrl Url used when registering test subscriptions","title":"SP values"},{"location":"v2/concepts/settings/#graph-values","text":"The graph values are described in the table below and come from registering an AAD Application . The permissions required by the registered application are dictated by the tests you want to run or resources you wish to test against. name description tenant Tenant to target for authentication and data (ex: contoso.onmicrosoft.com) id The application id secret The application secret","title":"Graph values"},{"location":"v2/concepts/settings/#create-settingsjs-file","text":"Copy the example file and rename it settings.js. Place the file in the root of your project. Update the settings as needed for your environment. If you are only doing SharePoint testing you can leave the graph section off and vice-versa. Also, if you are not testing anything with hooks you can leave off the notificationUrl.","title":"Create Settings.js file"},{"location":"v2/config-store/","text":"@pnp/config-store \u00b6 This module provides a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/sp @pnp/config-store --save See the topics below for usage: configuration providers","title":"@pnp/config-store"},{"location":"v2/config-store/#pnpconfig-store","text":"This module provides a way to load application configuration from one or more providers and share it across an application in a consistent way. A provider can be anything - but we have included one to load information from a SharePoint list. This library is most helpful for larger applications where a formal configuration model is needed.","title":"@pnp/config-store"},{"location":"v2/config-store/#getting-started","text":"Install the library and required dependencies npm install @pnp/sp @pnp/config-store --save See the topics below for usage: configuration providers","title":"Getting Started"},{"location":"v2/config-store/configuration/","text":"@pnp/config-store/configuration \u00b6 The main class exported from the config-store package is Settings. This is the class through which you will load and access your settings via providers . import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create an instance of the settings class, could be static and shared across your application // or built as needed. const settings = new Settings(); // you can add/update a single value using add settings.add(\"mykey\", \"myvalue\"); // you can also add/update a JSON value which will be stringified for you as a shorthand settings.addJSON(\"mykey2\", { field: 1, field2: 2, field3: 3, }); // and you can apply a plain object of keys/values that will be written as single values // this results in each enumerable property of the supplied object being added to the settings collection settings.apply({ field: 1, field2: 2, field3: 3, }); // and finally you can load values from a configuration provider const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // this will load values from the supplied list // by default the key will be from the Title field and the value from a column named Value await settings.load(provider); // once we have loaded values we can then read them const value = settings.get(\"mykey\"); // or read JSON that will be parsed for you from the store const value2 = settings.getJSON(\"mykey2\");","title":"@pnp/config-store/configuration"},{"location":"v2/config-store/configuration/#pnpconfig-storeconfiguration","text":"The main class exported from the config-store package is Settings. This is the class through which you will load and access your settings via providers . import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create an instance of the settings class, could be static and shared across your application // or built as needed. const settings = new Settings(); // you can add/update a single value using add settings.add(\"mykey\", \"myvalue\"); // you can also add/update a JSON value which will be stringified for you as a shorthand settings.addJSON(\"mykey2\", { field: 1, field2: 2, field3: 3, }); // and you can apply a plain object of keys/values that will be written as single values // this results in each enumerable property of the supplied object being added to the settings collection settings.apply({ field: 1, field2: 2, field3: 3, }); // and finally you can load values from a configuration provider const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // this will load values from the supplied list // by default the key will be from the Title field and the value from a column named Value await settings.load(provider); // once we have loaded values we can then read them const value = settings.get(\"mykey\"); // or read JSON that will be parsed for you from the store const value2 = settings.getJSON(\"mykey2\");","title":"@pnp/config-store/configuration"},{"location":"v2/config-store/providers/","text":"@pnp/config-store/providers \u00b6 Currently there is a single provider included in the library, but contributions of additional providers are welcome. SPListConfigurationProvider \u00b6 This provider is based on a SharePoint list it reads all of the rows and makes them available as a TypedHash . By default the column names used are Title for key and \"Value\" for value, but you can update these as needed. Additionally, the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); const settings = new Settings(); // load our values from the list await settings.load(provider); CachingConfigurationProvider \u00b6 Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a provider and caches the configuration in local or session storage. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // get an instance of the provider wrapped // you can optionally provide a key that will be used in the cache to the asCaching method const wrappedProvider = provider.asCaching(); // use that wrapped provider to populate the settings await settings.load(wrappedProvider);","title":"@pnp/config-store/providers"},{"location":"v2/config-store/providers/#pnpconfig-storeproviders","text":"Currently there is a single provider included in the library, but contributions of additional providers are welcome.","title":"@pnp/config-store/providers"},{"location":"v2/config-store/providers/#splistconfigurationprovider","text":"This provider is based on a SharePoint list it reads all of the rows and makes them available as a TypedHash . By default the column names used are Title for key and \"Value\" for value, but you can update these as needed. Additionally, the settings class supports the idea of last value in wins - so you can easily load multiple configurations. This helps to support a common scenario in the enterprise where you might have one main list for global configuration but some settings can be set at the web level. In this case you would first load the global, then the local settings and any local values will take precedence. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); const settings = new Settings(); // load our values from the list await settings.load(provider);","title":"SPListConfigurationProvider"},{"location":"v2/config-store/providers/#cachingconfigurationprovider","text":"Because making requests on each page load is very inefficient you can optionally use the caching configuration provider, which wraps a provider and caches the configuration in local or session storage. import { Web } from \"@pnp/sp/presets/all\"; import { Settings, SPListConfigurationProvider } from \"@pnp/config-store\"; // create a new provider instance const w = Web(\"https://mytenant.sharepoint.com/sites/dev\"); const provider = new SPListConfigurationProvider(w, \"myconfiglistname\"); // get an instance of the provider wrapped // you can optionally provide a key that will be used in the cache to the asCaching method const wrappedProvider = provider.asCaching(); // use that wrapped provider to populate the settings await settings.load(wrappedProvider);","title":"CachingConfigurationProvider"},{"location":"v2/contributing/","text":"Contributing to PnPjs \u00b6 Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality. Section Description Setup Dev Machine Covers setting up your machine to ensure you are ready to debug the solution Local Debug Configuration Discusses the steps required to establish local configuration used for debugging and running tests Debugging Describes how to debug PnPjs locally Extending the library Basic examples on how to extend the library such as adding a method or property Writing Tests How to write and debug tests Update Documentation Describes the steps required to edit and locally view the documentation Submit a Pull Request Outlines guidance for submitting a pull request Need Help? \u00b6 The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website.","title":"Contributing to PnPjs"},{"location":"v2/contributing/#contributing-to-pnpjs","text":"Thank you for your interest in contributing to PnPjs. We have updated our contribution section to make things easier to get started, debug the library locally, and learn how to extend the functionality. Section Description Setup Dev Machine Covers setting up your machine to ensure you are ready to debug the solution Local Debug Configuration Discusses the steps required to establish local configuration used for debugging and running tests Debugging Describes how to debug PnPjs locally Extending the library Basic examples on how to extend the library such as adding a method or property Writing Tests How to write and debug tests Update Documentation Describes the steps required to edit and locally view the documentation Submit a Pull Request Outlines guidance for submitting a pull request","title":"Contributing to PnPjs"},{"location":"v2/contributing/#need-help","text":"The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website.","title":"Need Help?"},{"location":"v2/contributing/debug-tests/","text":"Writing Tests \u00b6 With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place. How to write Tests \u00b6 We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts : import { getRandomString } from \"@pnp/core\"; import { testSettings } from \"../main\"; import { expect } from \"chai\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import { IList } from \"@pnp/sp/lists\"; describe(\"Items\", () => { // any tests that make a web request should be withing a block checking if web tests are enabled if (testSettings.enableWebTests) { // a block scoped var we will use across our tests let list: IList = null; // we use the before block to setup // executed before all the tests in this block, see the mocha docs for more details // mocha prefers using function vs arrow functions and this is recommended before(async function () { // execute a request to ensure we have a list const ler = await sp.web.lists.ensure(\"ItemTestList\", \"Used to test item operations\"); list = ler.list; // in this case we want to have some items in the list for testing so we add those // only if the list was just created if (ler.created) { // add a few items to get started const batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); await batch.execute(); } }); // this test has a label \"get items\" and is run via an async function it(\"get items\", async function () { // make a request for the list's items const items = await list.items(); // report that we expect that result to be an array with more than 0 items expect(items.length).to.be.gt(0); }); // ... remainder of code removed } } General Guidelines for Writing Tests \u00b6 Tests should operate within the site defined in testSettings Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll When writing tests you can use \"only\" and \"skip\" from mochajs to focus on only the tests you are writing Be sure to review the various options when running your tests If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description Next Steps \u00b6 Now that you've written tests to cover your changes you'll need to update the docs .","title":"Writing Tests"},{"location":"v2/contributing/debug-tests/#writing-tests","text":"With version 2 we have made a significant effort to improve out test coverage. To keep that up, all changes submitted will require one or more tests be included. For new functionality at least a basic test that the method executes is required. For bug fixes please include a test that would have caught the bug (i.e. fail before your fix) and passes with your fix in place.","title":"Writing Tests"},{"location":"v2/contributing/debug-tests/#how-to-write-tests","text":"We use Mocha and Chai for our testing framework. You can see many examples of writing tests within the ./test folder. Here is a sample with extra comments to help explain what's happening, taken from ./test/sp/items.ts : import { getRandomString } from \"@pnp/core\"; import { testSettings } from \"../main\"; import { expect } from \"chai\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import { IList } from \"@pnp/sp/lists\"; describe(\"Items\", () => { // any tests that make a web request should be withing a block checking if web tests are enabled if (testSettings.enableWebTests) { // a block scoped var we will use across our tests let list: IList = null; // we use the before block to setup // executed before all the tests in this block, see the mocha docs for more details // mocha prefers using function vs arrow functions and this is recommended before(async function () { // execute a request to ensure we have a list const ler = await sp.web.lists.ensure(\"ItemTestList\", \"Used to test item operations\"); list = ler.list; // in this case we want to have some items in the list for testing so we add those // only if the list was just created if (ler.created) { // add a few items to get started const batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); list.items.inBatch(batch).add({ Title: `Item ${getRandomString(4)}` }); await batch.execute(); } }); // this test has a label \"get items\" and is run via an async function it(\"get items\", async function () { // make a request for the list's items const items = await list.items(); // report that we expect that result to be an array with more than 0 items expect(items.length).to.be.gt(0); }); // ... remainder of code removed } }","title":"How to write Tests"},{"location":"v2/contributing/debug-tests/#general-guidelines-for-writing-tests","text":"Tests should operate within the site defined in testSettings Tests should be able to run multiple times on the same site, but do not need to cleanup after themselves Each test should be self contained and not depend on other tests, they can depend on work done in before or beforeAll When writing tests you can use \"only\" and \"skip\" from mochajs to focus on only the tests you are writing Be sure to review the various options when running your tests If you are writing a test and the endpoint doesn't support app only permissions, you can skip writing a test - but please note that in the PR description","title":"General Guidelines for Writing Tests"},{"location":"v2/contributing/debug-tests/#next-steps","text":"Now that you've written tests to cover your changes you'll need to update the docs .","title":"Next Steps"},{"location":"v2/contributing/debugging/","text":"Debugging \u00b6 Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on. Before proceeding be sure you have reviewed how to setup for local configuration and debugging. Debugging Library Features \u00b6 The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point. Basic SharePoint Testing \u00b6 You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules . All of the setup for the node client is handled within sp.ts using the settings from the local configuration . Basic Graph Testing \u00b6 Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit. All of the setup for the node client is handled within graph.ts using the settings from the local configuration . How to: Create a Debug Module \u00b6 If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git. Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content: // note we can use the actual package names for our imports (ex: @pnp/logging) import { Logger, LogLevel, ConsoleListener } from \"@pnp/logging\"; // using the all preset for simplicity in the example, selective imports work as expected import { sp, ListEnsureResult } from \"@pnp/sp/presets/all\"; declare var process: { exit(code?: number): void }; export async function MyDebug() { // configure your options // you can have different configs in different modules as needed for your testing/dev work sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret); }, }, }); // run some debugging const list = await sp.web.lists.ensure(\"MyFirstList\"); Logger.log({ data: list.created, level: LogLevel.Info, message: \"Was list created?\", }); if (list.created) { Logger.log({ data: list.data, level: LogLevel.Info, message: \"Raw data from list creation.\", }); } else { Logger.log({ data: null, level: LogLevel.Info, message: \"List already existed!\", }); } process.exit(0); } Update main.ts to launch your module \u00b6 First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this: // ... // comment out the example // import { Example } from \"./example\"; // Example(); import { MyDebug } from \"./mydebug\" MyDebug(); // ... Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file) Debug \u00b6 Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios. Debug Module Next Steps \u00b6 Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run. In Browser Debugging \u00b6 You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js , allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner. Start the local serve \u00b6 This will serve a package with ./debug/serve/main.ts as the entry. gulp serve Add reference to library \u00b6 Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
    You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files. Debug \u00b6 Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it. Next Steps \u00b6 You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser. Now you can learn about extending the library .","title":"Debugging"},{"location":"v2/contributing/debugging/#debugging","text":"Using the steps in this article you will be able to locally debug the library internals as well as new features you are working on. Before proceeding be sure you have reviewed how to setup for local configuration and debugging.","title":"Debugging"},{"location":"v2/contributing/debugging/#debugging-library-features","text":"The easiest way to debug the library when working on new features is using F5 in Visual Studio Code. This uses launch.json to build and run the library using ./debug/launch/main.ts as the entry point.","title":"Debugging Library Features"},{"location":"v2/contributing/debugging/#basic-sharepoint-testing","text":"You can start the base debugging case by hitting F5. Before you do place a break point in ./debug/launch/sp.ts. You can also place a break point within any of the libraries or modules. Feel free to edit the sp.ts file to try things out, debug suspected issues, or test new features, etc - but please don't commit any changes as this is a shared file. See the section on creating your own debug modules . All of the setup for the node client is handled within sp.ts using the settings from the local configuration .","title":"Basic SharePoint Testing"},{"location":"v2/contributing/debugging/#basic-graph-testing","text":"Testing and debugging Graph calls follows the same process as outlined for SharePoint, however you need to update main.ts to import graph instead of sp. You can place break points anywhere within the library code and they should be hit. All of the setup for the node client is handled within graph.ts using the settings from the local configuration .","title":"Basic Graph Testing"},{"location":"v2/contributing/debugging/#how-to-create-a-debug-module","text":"If you are working on multiple features or want to save sample code for various tasks you can create your own debugging modules and leave them in the debug/launch folder locally. The gitignore file is setup to ignore any files that aren't already in git. Using ./debug/launch/sp.ts as a reference create a file in the debug/launch folder, let's call it mydebug.ts and add this content: // note we can use the actual package names for our imports (ex: @pnp/logging) import { Logger, LogLevel, ConsoleListener } from \"@pnp/logging\"; // using the all preset for simplicity in the example, selective imports work as expected import { sp, ListEnsureResult } from \"@pnp/sp/presets/all\"; declare var process: { exit(code?: number): void }; export async function MyDebug() { // configure your options // you can have different configs in different modules as needed for your testing/dev work sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret); }, }, }); // run some debugging const list = await sp.web.lists.ensure(\"MyFirstList\"); Logger.log({ data: list.created, level: LogLevel.Info, message: \"Was list created?\", }); if (list.created) { Logger.log({ data: list.data, level: LogLevel.Info, message: \"Raw data from list creation.\", }); } else { Logger.log({ data: null, level: LogLevel.Info, message: \"List already existed!\", }); } process.exit(0); }","title":"How to: Create a Debug Module"},{"location":"v2/contributing/debugging/#update-maints-to-launch-your-module","text":"First comment out the import for the default example and then add the import and function call for yours, the updated launch/main.ts should look like this: // ... // comment out the example // import { Example } from \"./example\"; // Example(); import { MyDebug } from \"./mydebug\" MyDebug(); // ... Remember, please don't commit any changes to the shared files within the debug folder. (Unless you've found a bug that needs fixing in the original file)","title":"Update main.ts to launch your module"},{"location":"v2/contributing/debugging/#debug","text":"Place a break point within the mydebug.ts file and hit F5. Your module should run and your break point hit. You can then examine the contents of the objects and see the run time state. Remember, you can also set breakpoints within the package src folders to see exactly how things are working during your debugging scenarios.","title":"Debug"},{"location":"v2/contributing/debugging/#debug-module-next-steps","text":"Using this pattern you can create and preserve multiple debugging scenarios in separate modules locally - they won't be added to git. You just have to update main.ts to point to the one you want to run.","title":"Debug Module Next Steps"},{"location":"v2/contributing/debugging/#in-browser-debugging","text":"You can also serve files locally to debug as a user in the browser by serving code using ./debug/serve/main.ts as the entry. The file is served as https://localhost:8080/assets/pnp.js , allowing you to create a single page in your tenant for in browser testing. The remainder of this section describes the process to setup a SharePoint page to debug in this manner.","title":"In Browser Debugging"},{"location":"v2/contributing/debugging/#start-the-local-serve","text":"This will serve a package with ./debug/serve/main.ts as the entry. gulp serve","title":"Start the local serve"},{"location":"v2/contributing/debugging/#add-reference-to-library","text":"Within a SharePoint page add a script editor web part and then paste in the following code. The div is to give you a place to target with visual updates should you desire.
    You should see an alert with the current web's title using the default main.ts. Feel free to update main.ts to do whatever you would like, but remember not to commit changes to the shared files.","title":"Add reference to library"},{"location":"v2/contributing/debugging/#debug_1","text":"Refresh the page and open the developer tools in your browser of choice. If the pnp.js file is blocked due to security restrictions you will need to allow it.","title":"Debug"},{"location":"v2/contributing/debugging/#next-steps","text":"You can make changes to the library and immediately see them reflected in the browser. All files are watched so changes will be available as soon as webpack reloads the package. This allows you to rapidly test the library in the browser. Now you can learn about extending the library .","title":"Next Steps"},{"location":"v2/contributing/documentation/","text":"Documentation \u00b6 Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request. Writing Docs \u00b6 Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources. Building Docs Locally \u00b6 Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable . When executing the pip module on Windows you can prefix it with python -m . For example: python -m pip install mkdocs-material Install MkDocs pip install mkdocs Install the Material theme pip install mkdocs-material install the mkdocs-markdownextradata-plugin - this is used for the version variable pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) install redirect plugin - used to redirect from moved pages pip install mkdocs-redirects Serve it up mkdocs serve Open a browser to http://127.0.0.1:8000/ Please see the official mkdocs site for more details on working with mkdocs Next Steps \u00b6 After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request !","title":"Documentation"},{"location":"v2/contributing/documentation/#documentation","text":"Just like with tests we have invested much time in updating the documentation and when you make a change to the library you should update the associated documentation as part of the pull request.","title":"Documentation"},{"location":"v2/contributing/documentation/#writing-docs","text":"Our docs are all written in markdown and processed using MkDocs. You can use code blocks, tables, and other markdown formatting. You can review the other articles for examples on writing docs. Generally articles should focus on how to use the library and where appropriate link to official outside documents as needed. Official documentation could be Microsoft, other library project docs such as MkDocs, or other sources.","title":"Writing Docs"},{"location":"v2/contributing/documentation/#building-docs-locally","text":"Building the documentation locally can help you visualize change you are making to the docs. What you see locally will be what you see online. Documentation is built using MkDocs. You will need to latest version of Python (tested on version 3.7.1) and pip. If you're on the Windows operating system, make sure you have added Python to your Path environment variable . When executing the pip module on Windows you can prefix it with python -m . For example: python -m pip install mkdocs-material Install MkDocs pip install mkdocs Install the Material theme pip install mkdocs-material install the mkdocs-markdownextradata-plugin - this is used for the version variable pip install mkdocs-markdownextradata-plugin (doesn't work on Python v2.7) install redirect plugin - used to redirect from moved pages pip install mkdocs-redirects Serve it up mkdocs serve Open a browser to http://127.0.0.1:8000/ Please see the official mkdocs site for more details on working with mkdocs","title":"Building Docs Locally"},{"location":"v2/contributing/documentation/#next-steps","text":"After your changes are made, you've added/updated tests, and updated the docs you're ready to submit a pull request !","title":"Next Steps"},{"location":"v2/contributing/extending-the-library/","text":"Extending PnPjs \u00b6 This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property. At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the \"webs\" property is added to the web class. // TypeScript property, returning an interface public get webs(): IWebs { // using the Webs factory function and providing \"this\" as the first parameter return Webs(this); } Understanding Factory Functions \u00b6 PnPjs v2 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form. // create a constant which is a function of type ISPInvokableFactory having the name Webs // this is bound by the generic type param to return an IWebs instance // and it will use the _Webs concrete class to form the internal type of the invocable export const Webs = spInvokableFactory(_Webs); The ISPInvokableFactory type looks like: export type ISPInvokableFactory = (baseUrl: string | ISharePointQueryable, path?: string) => R; And the matching graph type: (f: any): (baseUrl: string | IGraphQueryable, path?: string) => R The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples. import { Web } from \"@pnp/sp/webs\"; // create a web from an absolute url const web = Web(\"https://tenant.sharepoint.com\"); // as an example, create a new web using the first as a base // targets: https://tenant.sharepoint.com/sites/dev const web2 = Web(web, \"sites/dev\"); // or you can add any path components you want, here as an example we access the current user property const cu = Web(web, \"currentuser\"); const currentUserInfo = cu(); Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their \"type\". It is an important concept when working with the library to always remember we are just building strings. Class structure \u00b6 Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method /* The concrete class implementation. This is never exported or shown directly to consumers of the library. It is wrapped by the Proxy we do expose. It extends the _SharePointQueryableInstance class for which there is a matching _SharePointQueryableCollection. The generic parameter defines the return type of a get operation and the invoked result. Classes can have methods and properties as normal. This one has a single property as a simple example */ export class _HubSite extends _SharePointQueryableInstance { /** * Gets the ISite instance associated with this hub site */ // the tag decorator is used to provide some additional telemetry on what methods are // being called. @tag(\"hs.getSite\") public async getSite(): Promise { // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result const d = await this.select(\"SiteUrl\")(); // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl return Site(d.SiteUrl); } } /* This defines the interface we export and expose to consumers. In most cases this extends the concrete object but may add or remove some methods/properties in special cases */ export interface IHubSite extends _HubSite { } /* This defines the HubSite factory function as discussed above binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite. This is understood to mean that HubSite is a factory function that returns a types of IHubSite which the spInvokableFactory will create using _HubSite as the concrete underlying type. */ export const HubSite = spInvokableFactory(_HubSite); Add a Property \u00b6 In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class. export class _View extends _SharePointQueryableInstance { // ... other code removed // add the property, and provide a return type // return types should be interfaces public get fields(): IViewFields { // we use the ViewFields factory function supplying \"this\" as the first parameter // this will create a url like \".../fields/viewfields\" due to the defaultPath decorator // on the _ViewFields class. This is equivalent to: ViewFields(this, \"viewfields\") return ViewFields(this); } // ... other code removed } There are many examples throughout the library that follow this pattern. Add a Method \u00b6 Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method: @defaultPath(\"items\") export class _Items extends _SharePointQueryableCollection { /** * Gets an Item by id * * @param id The integer id of the item to retrieve */ // we declare a method and set the return type to an interface public getById(id: number): IItem { // here we use the tag helper to add some telemetry to our request // we create a new IItem using the factory and appending the id value to the end // this gives us a valid url path to a single item .../items/getById(2) // we can then use the returned IItem to extend our chain or execute a request return tag.configure(Item(this).concat(`(${id})`), \"is.getById\"); } // ... other code removed } Web Request Method \u00b6 A second example is a method that performs a request. Here we use the _Item recycle method as an example: /** * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item. */ // we use the tag decorator to add telemetry @tag(\"i.recycle\") // we return a promise public recycle(): Promise { // we use the spPost method to post the request created by cloning our current instance IItem using // the Item factory and adding the path \"recycle\" to the end. Url will look like .../items/getById(2)/recycle return spPost(this.clone(Item, \"recycle\")); } Augment Using Selective Imports \u00b6 To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available. // import the addProp helper import { addProp } from \"@pnp/queryable\"; // import the _List concrete class from the types module (not the index!) import { _List } from \"../lists/types\"; // import the interface and factory we are going to add to the List import { Items, IItems } from \"./types\"; // This module declaration fixes up the types, allowing .items to appear in intellisense // when you import \"@pnp/sp/items/list\"; declare module \"../lists/types\" { // we need to extend the concrete type interface _List { readonly items: IItems; } // we need to extend the interface // this may not be strictly necessary as the IList interface extends _List so it // should pick up the same additions, but we have seen in some cases this does seem // to be required. So we include it for safety as it will all be removed during // transpilation we don't need to care about the extra code interface IList { readonly items: IItems; } } // finally we add the property to the _List class // this method call says add a property to _List named \"items\" and that property returns a result using the Items factory // The factory will be called with \"this\" when the property is accessed. If needed there is a fourth parameter to append additional path // information to the property url addProp(_List, \"items\", Items); General Rules for Extending PnPjs \u00b6 Only expose interfaces to consumers Use the factory functions except in very special cases Look for other properties and methods as examples Simple is always preferable, but not always possible - use your best judgement If you find yourself writing a ton of code to solve a problem you think should be easy, ask If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed Next Steps \u00b6 Now that you have extended the library you need to write a test to cover it!","title":"Extending PnPjs"},{"location":"v2/contributing/extending-the-library/#extending-pnpjs","text":"This article is targeted at people wishing to extend PnPjs itself, usually by adding a method or property. At the most basic level PnPjs is a set of libraries used to build and execute a web request and handle the response from that request. Conceptually each object in the fluent chain serves as input when creating the next object in the chain. This is how configuration, url, query, and other values are passed along. To get a sense for what this looks like see the code below. This is taken from inside the webs submodule and shows how the \"webs\" property is added to the web class. // TypeScript property, returning an interface public get webs(): IWebs { // using the Webs factory function and providing \"this\" as the first parameter return Webs(this); }","title":"Extending PnPjs"},{"location":"v2/contributing/extending-the-library/#understanding-factory-functions","text":"PnPjs v2 is designed to only expose interfaces and factory functions. Let's look at the Webs factory function, used above as an example. All factory functions in sp and graph have a similar form. // create a constant which is a function of type ISPInvokableFactory having the name Webs // this is bound by the generic type param to return an IWebs instance // and it will use the _Webs concrete class to form the internal type of the invocable export const Webs = spInvokableFactory(_Webs); The ISPInvokableFactory type looks like: export type ISPInvokableFactory = (baseUrl: string | ISharePointQueryable, path?: string) => R; And the matching graph type: (f: any): (baseUrl: string | IGraphQueryable, path?: string) => R The general idea of a factory function is that it takes two parameters. The first is either a string or Queryable derivative which forms base for the new object. The second is the next part of the url. In some cases (like the webs property example above) you will note there is no second parameter. Some classes are decorated with defaultPath, which automatically fills the second param. Don't worry too much right now about the deep internals of the library, let's instead focus on some concrete examples. import { Web } from \"@pnp/sp/webs\"; // create a web from an absolute url const web = Web(\"https://tenant.sharepoint.com\"); // as an example, create a new web using the first as a base // targets: https://tenant.sharepoint.com/sites/dev const web2 = Web(web, \"sites/dev\"); // or you can add any path components you want, here as an example we access the current user property const cu = Web(web, \"currentuser\"); const currentUserInfo = cu(); Now hey you might say - you can't create a request to current user using the Web factory. Well you can, since everything is just based on urls under the covers the actual factory names don't mean anything other than they have the appropriate properties and method hung off them. This is brought up as you will see in many cases objects being used to create queries within methods and properties that don't match their \"type\". It is an important concept when working with the library to always remember we are just building strings.","title":"Understanding Factory Functions"},{"location":"v2/contributing/extending-the-library/#class-structure","text":"Internally to the library we have a bit of complexity to make the whole invocable proxy architecture work and provide the typings folks expect. Here is an example implementation with extra comments explaining what is happening. You don't need to understand the entire stack to add a property or method /* The concrete class implementation. This is never exported or shown directly to consumers of the library. It is wrapped by the Proxy we do expose. It extends the _SharePointQueryableInstance class for which there is a matching _SharePointQueryableCollection. The generic parameter defines the return type of a get operation and the invoked result. Classes can have methods and properties as normal. This one has a single property as a simple example */ export class _HubSite extends _SharePointQueryableInstance { /** * Gets the ISite instance associated with this hub site */ // the tag decorator is used to provide some additional telemetry on what methods are // being called. @tag(\"hs.getSite\") public async getSite(): Promise { // we execute a request using this instance, selecting the SiteUrl property, and invoking it immediately and awaiting the result const d = await this.select(\"SiteUrl\")(); // we then return a new ISite instance created from the Site factory using the returned SiteUrl property as the baseUrl return Site(d.SiteUrl); } } /* This defines the interface we export and expose to consumers. In most cases this extends the concrete object but may add or remove some methods/properties in special cases */ export interface IHubSite extends _HubSite { } /* This defines the HubSite factory function as discussed above binding the spInvokableFactory to a generic param of IHubSite and a param of _HubSite. This is understood to mean that HubSite is a factory function that returns a types of IHubSite which the spInvokableFactory will create using _HubSite as the concrete underlying type. */ export const HubSite = spInvokableFactory(_HubSite);","title":"Class structure"},{"location":"v2/contributing/extending-the-library/#add-a-property","text":"In most cases you won't need to create the class, interface, or factory - you just want to add a property or method. An example of this is sp.web.lists. web is a property of sp and lists is a property of web. You can have a look at those classes as examples. Let's have a look at the fields on the _View class. export class _View extends _SharePointQueryableInstance { // ... other code removed // add the property, and provide a return type // return types should be interfaces public get fields(): IViewFields { // we use the ViewFields factory function supplying \"this\" as the first parameter // this will create a url like \".../fields/viewfields\" due to the defaultPath decorator // on the _ViewFields class. This is equivalent to: ViewFields(this, \"viewfields\") return ViewFields(this); } // ... other code removed } There are many examples throughout the library that follow this pattern.","title":"Add a Property"},{"location":"v2/contributing/extending-the-library/#add-a-method","text":"Adding a method is just like adding a property with the key difference that a method usually does something like make a web request or act like a property but take parameters. Let's look at the _Items getById method: @defaultPath(\"items\") export class _Items extends _SharePointQueryableCollection { /** * Gets an Item by id * * @param id The integer id of the item to retrieve */ // we declare a method and set the return type to an interface public getById(id: number): IItem { // here we use the tag helper to add some telemetry to our request // we create a new IItem using the factory and appending the id value to the end // this gives us a valid url path to a single item .../items/getById(2) // we can then use the returned IItem to extend our chain or execute a request return tag.configure(Item(this).concat(`(${id})`), \"is.getById\"); } // ... other code removed }","title":"Add a Method"},{"location":"v2/contributing/extending-the-library/#web-request-method","text":"A second example is a method that performs a request. Here we use the _Item recycle method as an example: /** * Moves the list item to the Recycle Bin and returns the identifier of the new Recycle Bin item. */ // we use the tag decorator to add telemetry @tag(\"i.recycle\") // we return a promise public recycle(): Promise { // we use the spPost method to post the request created by cloning our current instance IItem using // the Item factory and adding the path \"recycle\" to the end. Url will look like .../items/getById(2)/recycle return spPost(this.clone(Item, \"recycle\")); }","title":"Web Request Method"},{"location":"v2/contributing/extending-the-library/#augment-using-selective-imports","text":"To understand is how to extend functionality within the selective imports structures look at list.ts file in the items submodule. Here you can see the code below, with extra comments to explain what is happening. Again, you will see this pattern repeated throughout the library so there are many examples available. // import the addProp helper import { addProp } from \"@pnp/queryable\"; // import the _List concrete class from the types module (not the index!) import { _List } from \"../lists/types\"; // import the interface and factory we are going to add to the List import { Items, IItems } from \"./types\"; // This module declaration fixes up the types, allowing .items to appear in intellisense // when you import \"@pnp/sp/items/list\"; declare module \"../lists/types\" { // we need to extend the concrete type interface _List { readonly items: IItems; } // we need to extend the interface // this may not be strictly necessary as the IList interface extends _List so it // should pick up the same additions, but we have seen in some cases this does seem // to be required. So we include it for safety as it will all be removed during // transpilation we don't need to care about the extra code interface IList { readonly items: IItems; } } // finally we add the property to the _List class // this method call says add a property to _List named \"items\" and that property returns a result using the Items factory // The factory will be called with \"this\" when the property is accessed. If needed there is a fourth parameter to append additional path // information to the property url addProp(_List, \"items\", Items);","title":"Augment Using Selective Imports"},{"location":"v2/contributing/extending-the-library/#general-rules-for-extending-pnpjs","text":"Only expose interfaces to consumers Use the factory functions except in very special cases Look for other properties and methods as examples Simple is always preferable, but not always possible - use your best judgement If you find yourself writing a ton of code to solve a problem you think should be easy, ask If you find yourself deep within the core classes or odata library trying to make a change, ask - changes to the core classes are rarely needed","title":"General Rules for Extending PnPjs"},{"location":"v2/contributing/extending-the-library/#next-steps","text":"Now that you have extended the library you need to write a test to cover it!","title":"Next Steps"},{"location":"v2/contributing/local-debug-configuration/","text":"Local Debugging Configuration \u00b6 This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly). Create settings.js \u00b6 Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. For more information the settings file please see Settings Minimal Configuration \u00b6 You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag. The following configuration file allows you to run all the tests that do not contact services. var sets = { testing: { enableWebTests: false, } } module.exports = sets; Test your setup \u00b6 If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.","title":"Local Debugging Configuration"},{"location":"v2/contributing/local-debug-configuration/#local-debugging-configuration","text":"This article covers the local setup required to debug the library and run tests. This only needs to be done once (unless you update the app registrations, then you just need to update the settings.js file accordingly).","title":"Local Debugging Configuration"},{"location":"v2/contributing/local-debug-configuration/#create-settingsjs","text":"Both local debugging and tests make use of a settings.js file located in the root of the project. Ensure you create a settings.js files by copying settings.example.js and renaming it to settings.js. For more information the settings file please see Settings","title":"Create settings.js"},{"location":"v2/contributing/local-debug-configuration/#minimal-configuration","text":"You can control which tests are run by including or omitting sp and graph sections. If sp is present and graph is not, only sp tests are run. Include both and all tests are run, respecting the enableWebTests flag. The following configuration file allows you to run all the tests that do not contact services. var sets = { testing: { enableWebTests: false, } } module.exports = sets;","title":"Minimal Configuration"},{"location":"v2/contributing/local-debug-configuration/#test-your-setup","text":"If you hit F5 in VSCode now you should be able to see the full response from getting the web's title in the internal console window. If not, ensure that you have properly updated the settings file and registered the add-in perms correctly.","title":"Test your setup"},{"location":"v2/contributing/pull-requests/","text":"Submitting Pull Requests \u00b6 Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release. Target your pull requests to the version-2 branch Add/Update any relevant docs articles in the relevant package's docs folder related to your changes Include a test for any new functionality and ensure all existing tests are passing by running npm test Ensure linting checks pass by typing npm run lint Ensure everything works for a build by running npm run package Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :) If you need to target a PR for version 1, please target the \"version-1\" branch Sharing is Caring - Pull Request Guidance \u00b6 The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website. Next Steps \u00b6 Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted. Thank you for helping PnPjs grow and improve!!","title":"Submitting Pull Requests"},{"location":"v2/contributing/pull-requests/#submitting-pull-requests","text":"Pull requests may be large or small - adding whole new features or fixing some misspellings. Regardless, they are all appreciated and help improve the library for everyone! By following the below guidelines we'll have an easier time merging your work and getting it into the next release. Target your pull requests to the version-2 branch Add/Update any relevant docs articles in the relevant package's docs folder related to your changes Include a test for any new functionality and ensure all existing tests are passing by running npm test Ensure linting checks pass by typing npm run lint Ensure everything works for a build by running npm run package Keep your PRs as simple as possible and describe the changes to help the reviewer understand your work If you have an idea for a larger change to the library please open an issue and let's discuss before you invest many hours - these are very welcome but want to ensure it is something we can merge before you spend the time :) If you need to target a PR for version 1, please target the \"version-1\" branch","title":"Submitting Pull Requests"},{"location":"v2/contributing/pull-requests/#sharing-is-caring-pull-request-guidance","text":"The PnP \"Sharing Is Caring\" initiative teaches the basics around making changes in GitHub, submitting pull requests to the PnP & Microsoft 365 open-source repositories such as PnPjs. Every month, we provide multiple live hands-on sessions that walk attendees through the process of using and contributing to PnP initiatives. To learn more and register for an upcoming session, please visit the Sharing is Caring website.","title":"Sharing is Caring - Pull Request Guidance"},{"location":"v2/contributing/pull-requests/#next-steps","text":"Now that you've submitted your PR please keep an eye on it as we might have questions. Once an initial review is complete we'll tag it with the expected version number for which it is targeted. Thank you for helping PnPjs grow and improve!!","title":"Next Steps"},{"location":"v2/contributing/setup-dev-machine/","text":"Setting up your Developer Machine \u00b6 If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging . Setup your development environment \u00b6 These steps will help you get your environment setup for contributing to the core library. Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like. Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). This library requires node >= 10.18.0 On Windows: Install Python [Optional] Install the tslint extension in VS Code: Press Shift + Ctrl + \"p\" to open the command panel Begin typing \"install extension\" and select the command when it appears in view Begin typing \"tslint\" and select the package when it appears in view Restart Code after installation Fork The Repo \u00b6 All of our contributions come via pull requests and you'll need to fork the repository Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool. Once you have the code locally, navigate to the root of the project in your console. Type the following command: npm install Follow the guidance to complete the one-time local configuration required to debug and run tests. Then you can follow the guidance in the debugging article.","title":"Setting up your Developer Machine"},{"location":"v2/contributing/setup-dev-machine/#setting-up-your-developer-machine","text":"If you are a longtime client side developer you likely have your machine already configured and can skip to forking the repo and debugging .","title":"Setting up your Developer Machine"},{"location":"v2/contributing/setup-dev-machine/#setup-your-development-environment","text":"These steps will help you get your environment setup for contributing to the core library. Install Visual Studio Code - this is the development environment we use so the contribution sections expect you are as well. If you prefer you can use Visual Studio or any editor you like. Install Node JS - this provides two key capabilities; the first is the nodejs server which will act as our development server (think iisexpress), the second is npm a package manager (think nuget). This library requires node >= 10.18.0 On Windows: Install Python [Optional] Install the tslint extension in VS Code: Press Shift + Ctrl + \"p\" to open the command panel Begin typing \"install extension\" and select the command when it appears in view Begin typing \"tslint\" and select the package when it appears in view Restart Code after installation","title":"Setup your development environment"},{"location":"v2/contributing/setup-dev-machine/#fork-the-repo","text":"All of our contributions come via pull requests and you'll need to fork the repository Now we need to fork and clone the git repository. This can be done using your console or using your preferred Git GUI tool. Once you have the code locally, navigate to the root of the project in your console. Type the following command: npm install Follow the guidance to complete the one-time local configuration required to debug and run tests. Then you can follow the guidance in the debugging article.","title":"Fork The Repo"},{"location":"v2/graph/","text":"@pnp/graph \u00b6 This package contains the fluent api used to call the graph rest services. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application and access the root sp object import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; (function main() { // here we will load the current web's properties graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); }); })() Getting Started with SharePoint Framework \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application, update OnInit, and access the root sp object in render import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; // here we will load the current web's properties graph.groups().then(groups => { this.domElement.innerHTML = `Groups:
      ${groups.map(g => `
    • ${g.displayName}
    • `).join(\"\")}
    `; }); } Getting Started on Nodejs \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { graph } from \"@pnp/graph\"; import { AdalFetchClient } from \"@pnp/nodejs\"; import \"@pnp/graph/groups\"; // do this once per page load graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}.onmicrosoft.com\", \"AAD Application Id\", \"AAD Application Secret\"); }, }, }); // here we will load the groups information graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); });","title":"@pnp/graph"},{"location":"v2/graph/#pnpgraph","text":"This package contains the fluent api used to call the graph rest services.","title":"@pnp/graph"},{"location":"v2/graph/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application and access the root sp object import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; (function main() { // here we will load the current web's properties graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); }); })()","title":"Getting Started"},{"location":"v2/graph/#getting-started-with-sharepoint-framework","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph --save Import the library into your application, update OnInit, and access the root sp object in render import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present graph.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; // here we will load the current web's properties graph.groups().then(groups => { this.domElement.innerHTML = `Groups:
      ${groups.map(g => `
    • ${g.displayName}
    • `).join(\"\")}
    `; }); }","title":"Getting Started with SharePoint Framework"},{"location":"v2/graph/#getting-started-on-nodejs","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable @pnp/graph @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { graph } from \"@pnp/graph\"; import { AdalFetchClient } from \"@pnp/nodejs\"; import \"@pnp/graph/groups\"; // do this once per page load graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}.onmicrosoft.com\", \"AAD Application Id\", \"AAD Application Secret\"); }, }, }); // here we will load the groups information graph.groups().then(g => { console.log(`Groups: ${JSON.stringify(g, null, 4)}`); });","title":"Getting Started on Nodejs"},{"location":"v2/graph/calendars/","text":"@pnp/graph/calendars \u00b6 Calendars exist in Outlook and can belong to either a user or group. With @pnp/graph@<=2.0.6 , only events for a user and group's default calendar could be fetched/created/updated. In versions 2.0.7 and up, all calendars and their events can be fetched. More information can be found in the official Graph documentation: Calendar Resource Type Event Resource Type ICalendar, ICalendars \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/calendars\"; Preset: All import { graph } from \"@pnp/graph/presets/all\"; Get All Calendars For a User \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars(); const myCalendars = await graph.me.calendars(); Get a Specific Calendar For a User \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)(); const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)(); Get a User's Default Calendar \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar(); const myCalendar = await graph.me.calendar(); Get Events For a User's Default Calendar \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // You can get the default calendar events const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events(); // or get all events for the user const events = await graph.users.getById('user@tenant.onmicrosoft.com').events(); // You can get my default calendar events const events = await graph.me.calendar.events(); // or get all events for me const events = await graph.me.events(); Get Events By ID \u00b6 You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA=='; const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; // Get events by ID const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID); const events = await graph.me.events.getByID(EventID); // Get an event by ID from a specific calendar const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID); const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID); Create Events \u00b6 This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add( { \"subject\": \"Let's go for lunch\", \"body\": { \"contentType\": \"HTML\", \"content\": \"Does late morning work for you?\" }, \"start\": { \"dateTime\": \"2017-04-15T12:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"end\": { \"dateTime\": \"2017-04-15T14:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"location\":{ \"displayName\":\"Harry's Bar\" }, \"attendees\": [ { \"emailAddress\": { \"address\":\"samanthab@contoso.onmicrosoft.com\", \"name\": \"Samantha Booth\" }, \"type\": \"required\" } ] }); Update Events \u00b6 This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({ reminderMinutesBeforeStart: 99, }); Delete Event \u00b6 This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete(); await graph.me.events.getById(EVENT_ID).delete(); Get Calendar for a Group \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar(); Get Events for a Group \u00b6 import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; // You can do one of const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events(); // or const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events(); Get Calendar View \u00b6 Added in 2.0.7 Gets the events in a calendar during a specified date range. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // basic request, note need to invoke the returned queryable const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\")(); // you can use select, top, etc to filter your returned results const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)(); // you can specify times along with the dates const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")(); const view4 = await graph.me.calendarView(\"2020-01-01\", \"2020-03-01\")();","title":"@pnp/graph/calendars"},{"location":"v2/graph/calendars/#pnpgraphcalendars","text":"Calendars exist in Outlook and can belong to either a user or group. With @pnp/graph@<=2.0.6 , only events for a user and group's default calendar could be fetched/created/updated. In versions 2.0.7 and up, all calendars and their events can be fetched. More information can be found in the official Graph documentation: Calendar Resource Type Event Resource Type","title":"@pnp/graph/calendars"},{"location":"v2/graph/calendars/#icalendar-icalendars","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/calendars\"; Preset: All import { graph } from \"@pnp/graph/presets/all\";","title":"ICalendar, ICalendars"},{"location":"v2/graph/calendars/#get-all-calendars-for-a-user","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendars = await graph.users.getById('user@tenant.onmicrosoft.com').calendars(); const myCalendars = await graph.me.calendars();","title":"Get All Calendars For a User"},{"location":"v2/graph/calendars/#get-a-specific-calendar-for-a-user","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CALENDAR_ID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getById(CALENDAR_ID)(); const myCalendar = await graph.me.calendars.getById(CALENDAR_ID)();","title":"Get a Specific Calendar For a User"},{"location":"v2/graph/calendars/#get-a-users-default-calendar","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const calendar = await graph.users.getById('user@tenant.onmicrosoft.com').calendar(); const myCalendar = await graph.me.calendar();","title":"Get a User's Default Calendar"},{"location":"v2/graph/calendars/#get-events-for-a-users-default-calendar","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // You can get the default calendar events const events = await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events(); // or get all events for the user const events = await graph.users.getById('user@tenant.onmicrosoft.com').events(); // You can get my default calendar events const events = await graph.me.calendar.events(); // or get all events for me const events = await graph.me.events();","title":"Get Events For a User's Default Calendar"},{"location":"v2/graph/calendars/#get-events-by-id","text":"You can use .events.getByID to search through all the events in all calendars or narrow the request to a specific calendar. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const CalendarID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA=='; const EventID = 'AQMkAGZjNmY0MDN3LRI3YTYtNDQAFWQtOWNhZC04MmY3MGYxODkeOWUARgAAA-xUBMMopY1NkrWA0qGcXHsHAG4I-wMXjoRMkgRnRetM5oIAAAIBBgAAAG4I-wMXjoRMkgRnRetM5oIAAAIsYgAAAA=='; // Get events by ID const event = await graph.users.getById('user@tenant.onmicrosoft.com').events.getByID(EventID); const events = await graph.me.events.getByID(EventID); // Get an event by ID from a specific calendar const event = await graph.users.getById('user@tenant.onmicrosoft.com').calendars.getByID(CalendarID).events.getByID(EventID); const events = await graph.me.calendars.getByID(CalendarID).events.getByID(EventID);","title":"Get Events By ID"},{"location":"v2/graph/calendars/#create-events","text":"This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.add( { \"subject\": \"Let's go for lunch\", \"body\": { \"contentType\": \"HTML\", \"content\": \"Does late morning work for you?\" }, \"start\": { \"dateTime\": \"2017-04-15T12:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"end\": { \"dateTime\": \"2017-04-15T14:00:00\", \"timeZone\": \"Pacific Standard Time\" }, \"location\":{ \"displayName\":\"Harry's Bar\" }, \"attendees\": [ { \"emailAddress\": { \"address\":\"samanthab@contoso.onmicrosoft.com\", \"name\": \"Samantha Booth\" }, \"type\": \"required\" } ] });","title":"Create Events"},{"location":"v2/graph/calendars/#update-events","text":"This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').calendar.events.getById(EVENT_ID).update({ reminderMinutesBeforeStart: 99, });","title":"Update Events"},{"location":"v2/graph/calendars/#delete-event","text":"This will work on any IEvents objects (e.g. anything accessed using an events key). import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; const EVENT_ID = 'BBMkAGZjNmY6MDM3LWI3YTYtNERhZC05Y2FkLTgyZjcwZjE4OTI5ZQBGAAAAAAD8VQTDKKWNTY61gNKhnFzLBwBuCP8DF46ETJIEZ0XrTOaCAAAAAAENAABuCP8DF46ETJFEZ0EnTOaCAAFvdoJvAAA='; await graph.users.getById('user@tenant.onmicrosoft.com').events.getById(EVENT_ID).delete(); await graph.me.events.getById(EVENT_ID).delete();","title":"Delete Event"},{"location":"v2/graph/calendars/#get-calendar-for-a-group","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; const calendar = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar();","title":"Get Calendar for a Group"},{"location":"v2/graph/calendars/#get-events-for-a-group","text":"import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/groups'; // You can do one of const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').calendar.events(); // or const events = await graph.groups.getById('21aaf779-f6d8-40bd-88c2-4a03f456ee82').events();","title":"Get Events for a Group"},{"location":"v2/graph/calendars/#get-calendar-view","text":"Added in 2.0.7 Gets the events in a calendar during a specified date range. import { graph } from '@pnp/graph'; import '@pnp/graph/calendars'; import '@pnp/graph/users'; // basic request, note need to invoke the returned queryable const view = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\")(); // you can use select, top, etc to filter your returned results const view2 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01\", \"2020-03-01\").select(\"subject\").top(3)(); // you can specify times along with the dates const view3 = await graph.users.getById('user@tenant.onmicrosoft.com').calendarView(\"2020-01-01T19:00:00-08:00\", \"2020-03-01T19:00:00-08:00\")(); const view4 = await graph.me.calendarView(\"2020-01-01\", \"2020-03-01\")();","title":"Get Calendar View"},{"location":"v2/graph/contacts/","text":"@pnp/graph/contacts \u00b6 The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described you can add and edit both contacts and folders in a users Outlook. More information can be found in the official Graph documentation: Contact Resource Type IContact, IContacts, IContactFolder, IContactFolders \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/contacts\"; Preset: All import { graph } from \"@pnp/graph/presets/all\"; Set up notes \u00b6 To make user calls you can use getById where the id is the users email address. Contact ID, Folder ID, and Parent Folder ID use the following format \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\" Get all of the Contacts \u00b6 Gets a list of all the contacts for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts(); const contacts2 = await graph.me.contacts(); Get Contact by Id \u00b6 Gets a specific contact by ID for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)(); const contact2 = await graph.me.contacts.getById(contactID)(); Add a new Contact \u00b6 Adds a new contact for the user. import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); Update a Contact \u00b6 Updates a specific contact by ID for teh designated user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: \"1986-05-30\" }); const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: \"1986-05-30\" }); Delete a Contact \u00b6 Delete a contact from the list of contacts for a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete(); const delContact2 = await graph.me.contacts.getById(contactID).delete(); Get all of the Contact Folders \u00b6 Get all the folders for the designated user's contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders(); const contactFolders2 = await graph.me.contactFolders(); Get Contact Folder by Id \u00b6 Get a contact folder by ID for the specified user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)(); const contactFolder2 = await graph.me.contactFolders.getById(folderID)(); Add a new Contact Folder \u00b6 Add a new folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const parentFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=\"; const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add(\"New Folder\", parentFolderID); const addedContactFolder2 = await graph.me.contactFolders.add(\"New Folder\", parentFolderID); Update a Contact Folder \u00b6 Update an existing folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: \"Updated Folder\" }); const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: \"Updated Folder\" }); Delete a Contact Folder \u00b6 Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete(); const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete(); Get all of the Contacts from the Contact Folder \u00b6 Get all the contacts in a folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts(); const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts(); Get Child Folders of the Contact Folder \u00b6 Get child folders from contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders(); const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders(); Add a new Child Folder \u00b6 Add a new child folder to a contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID); const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID); Get Child Folder by Id \u00b6 Get child folder by ID from user contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)(); const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)(); Add Contact in Child Folder of Contact Folder \u00b6 Add a new contact to a child folder import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"./@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);","title":"@pnp/graph/contacts"},{"location":"v2/graph/contacts/#pnpgraphcontacts","text":"The ability to manage contacts and folders in Outlook is a capability introduced in version 1.2.2 of @pnp/graph. Through the methods described you can add and edit both contacts and folders in a users Outlook. More information can be found in the official Graph documentation: Contact Resource Type","title":"@pnp/graph/contacts"},{"location":"v2/graph/contacts/#icontact-icontacts-icontactfolder-icontactfolders","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/contacts\"; Preset: All import { graph } from \"@pnp/graph/presets/all\";","title":"IContact, IContacts, IContactFolder, IContactFolders"},{"location":"v2/graph/contacts/#set-up-notes","text":"To make user calls you can use getById where the id is the users email address. Contact ID, Folder ID, and Parent Folder ID use the following format \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"","title":"Set up notes"},{"location":"v2/graph/contacts/#get-all-of-the-contacts","text":"Gets a list of all the contacts for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contacts = await graph.users.getById('user@tenant.onmicrosoft.com').contacts(); const contacts2 = await graph.me.contacts();","title":"Get all of the Contacts"},{"location":"v2/graph/contacts/#get-contact-by-id","text":"Gets a specific contact by ID for the user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const contact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID)(); const contact2 = await graph.me.contacts.getById(contactID)();","title":"Get Contact by Id"},{"location":"v2/graph/contacts/#add-a-new-contact","text":"Adds a new contact for the user. import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);","title":"Add a new Contact"},{"location":"v2/graph/contacts/#update-a-contact","text":"Updates a specific contact by ID for teh designated user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const updContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).update({birthday: \"1986-05-30\" }); const updContact2 = await graph.me.contacts.getById(contactID).update({birthday: \"1986-05-30\" });","title":"Update a Contact"},{"location":"v2/graph/contacts/#delete-a-contact","text":"Delete a contact from the list of contacts for a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwBGAAAAAAC75QV12PBiRIjb8MNVIrJrBwBgs0NT6NreR57m1u_D8SpPAAAAAAEOAABgs0NT6NreR57m1u_D8SpPAAFCCnApAAA=\"; const delContact = await graph.users.getById('user@tenant.onmicrosoft.com').contacts.getById(contactID).delete(); const delContact2 = await graph.me.contacts.getById(contactID).delete();","title":"Delete a Contact"},{"location":"v2/graph/contacts/#get-all-of-the-contact-folders","text":"Get all the folders for the designated user's contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const contactFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders(); const contactFolders2 = await graph.me.contactFolders();","title":"Get all of the Contact Folders"},{"location":"v2/graph/contacts/#get-contact-folder-by-id","text":"Get a contact folder by ID for the specified user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID)(); const contactFolder2 = await graph.me.contactFolders.getById(folderID)();","title":"Get Contact Folder by Id"},{"location":"v2/graph/contacts/#add-a-new-contact-folder","text":"Add a new folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const parentFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAAAAAEOAAA=\"; const addedContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.add(\"New Folder\", parentFolderID); const addedContactFolder2 = await graph.me.contactFolders.add(\"New Folder\", parentFolderID);","title":"Add a new Contact Folder"},{"location":"v2/graph/contacts/#update-a-contact-folder","text":"Update an existing folder in the users contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const updContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).update({displayName: \"Updated Folder\" }); const updContactFolder2 = await graph.me.contactFolders.getById(folderID).update({displayName: \"Updated Folder\" });","title":"Update a Contact Folder"},{"location":"v2/graph/contacts/#delete-a-contact-folder","text":"Delete a folder from the users contacts list. Deleting a folder deletes the contacts in that folder. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const delContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).delete(); const delContactFolder2 = await graph.me.contactFolders.getById(folderID).delete();","title":"Delete a Contact Folder"},{"location":"v2/graph/contacts/#get-all-of-the-contacts-from-the-contact-folder","text":"Get all the contacts in a folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const contactsInContactFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).contacts(); const contactsInContactFolder2 = await graph.me.contactFolders.getById(folderID).contacts();","title":"Get all of the Contacts from the Contact Folder"},{"location":"v2/graph/contacts/#get-child-folders-of-the-contact-folder","text":"Get child folders from contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const childFolders = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders(); const childFolders2 = await graph.me.contactFolders.getById(folderID).childFolders();","title":"Get Child Folders of the Contact Folder"},{"location":"v2/graph/contacts/#add-a-new-child-folder","text":"Add a new child folder to a contact folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const addedChildFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID); const addedChildFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.add(\"Sub Folder\", folderID);","title":"Add a new Child Folder"},{"location":"v2/graph/contacts/#get-child-folder-by-id","text":"Get child folder by ID from user contacts import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const childFolder = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID)(); const childFolder2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID)();","title":"Get Child Folder by Id"},{"location":"v2/graph/contacts/#add-contact-in-child-folder-of-contact-folder","text":"Add a new contact to a child folder import { graph } from \"@pnp/graph\"; import { EmailAddress } from \"./@microsoft/microsoft-graph-types\"; import \"@pnp/graph/users\" import \"@pnp/graph/contacts\" const folderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqH9AAA=\"; const subFolderID = \"AAMkADY1OTQ5MTM0LTU2OTktNDI0Yy1iODFjLWNiY2RmMzNjODUxYwAuAAAAAAC75QV12PBiRIjb8MNVIrJrAQBgs0NT6NreR57m1u_D8SpPAAFCCqIZAAA=\"; const addedContact = await graph.users.getById('user@tenant.onmicrosoft.com').contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']); const addedContact2 = await graph.me.contactFolders.getById(folderID).childFolders.getById(subFolderID).contacts.add('Pavel', 'Bansky', [{address: 'pavelb@fabrikam.onmicrosoft.com', name: 'Pavel Bansky' }], ['+1 732 555 0102']);","title":"Add Contact in Child Folder of Contact Folder"},{"location":"v2/graph/directoryobjects/","text":"@pnp/graph/directoryObjects \u00b6 Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types. More information can be found in the official Graph documentation: DirectoryObject Resource Type IDirectoryObject, IDirectoryObjects \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; Preset: All import { graph } from \"@pnp/sp/presets/all\"; The groups and directory roles for the user \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf(); const memberOf2 = await graph.me.memberOf(); Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/groups\" const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups(); const memberGroups2 = await graph.me.getMemberGroups(); // Returns only security enabled groups const memberGroups3 = await graph.me.getMemberGroups(true); const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups(); Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects(); const memberObjects2 = await graph.me.getMemberObjects(); // Returns only security enabled groups const memberObjects3 = await graph.me.getMemberObjects(true); const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects(); Check for membership in a specified list of groups \u00b6 And returns from that list those groups of which the specified user, group, or directory object is a member import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers2 = await graph.me.checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); Get directoryObject by Id \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26'); Delete directoryObject \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()","title":"@pnp/graph/directoryObjects"},{"location":"v2/graph/directoryobjects/#pnpgraphdirectoryobjects","text":"Represents an Azure Active Directory object. The directoryObject type is the base type for many other directory entity types. More information can be found in the official Graph documentation: DirectoryObject Resource Type","title":"@pnp/graph/directoryObjects"},{"location":"v2/graph/directoryobjects/#idirectoryobject-idirectoryobjects","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; Preset: All import { graph } from \"@pnp/sp/presets/all\";","title":"IDirectoryObject, IDirectoryObjects"},{"location":"v2/graph/directoryobjects/#the-groups-and-directory-roles-for-the-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" const memberOf = await graph.users.getById('user@tenant.onmicrosoft.com').memberOf(); const memberOf2 = await graph.me.memberOf();","title":"The groups and directory roles for the user"},{"location":"v2/graph/directoryobjects/#return-all-the-groups-the-user-group-or-directoryobject-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/groups\" const memberGroups = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberGroups(); const memberGroups2 = await graph.me.getMemberGroups(); // Returns only security enabled groups const memberGroups3 = await graph.me.getMemberGroups(true); const memberGroups4 = await graph.groups.getById('user@tenant.onmicrosoft.com').getMemberGroups();","title":"Return all the groups the user, group or directoryObject is a member of. Add true parameter to return only security enabled groups"},{"location":"v2/graph/directoryobjects/#returns-all-the-groups-administrative-units-and-directory-roles-that-a-user-group-or-directory-object-is-a-member-of-add-true-parameter-to-return-only-security-enabled-groups","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const memberObjects = await graph.users.getById('user@tenant.onmicrosoft.com').getMemberObjects(); const memberObjects2 = await graph.me.getMemberObjects(); // Returns only security enabled groups const memberObjects3 = await graph.me.getMemberObjects(true); const memberObjects4 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').getMemberObjects();","title":"Returns all the groups, administrative units and directory roles that a user, group, or directory object is a member of. Add true parameter to return only security enabled groups"},{"location":"v2/graph/directoryobjects/#check-for-membership-in-a-specified-list-of-groups","text":"And returns from that list those groups of which the specified user, group, or directory object is a member import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/groups\"; const checkedMembers = await graph.users.getById('user@tenant.onmicrosoft.com').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers2 = await graph.me.checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]); const checkedMembers3 = await graph.groups.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').checkMemberGroups([\"c2fb52d1-5c60-42b1-8c7e-26ce8dc1e741\",\"2001bb09-1d46-40a6-8176-7bb867fb75aa\"]);","title":"Check for membership in a specified list of groups"},{"location":"v2/graph/directoryobjects/#get-directoryobject-by-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const dirObject = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26');","title":"Get directoryObject by Id"},{"location":"v2/graph/directoryobjects/#delete-directoryobject","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/directory-objects\"; const deleted = await graph.directoryObjects.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').delete()","title":"Delete directoryObject"},{"location":"v2/graph/groups/","text":"@pnp/graph/groups \u00b6 Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent. Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups. You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation . IGroup, IGroups \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups} from \"@pnp/graph/groups\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; Preset: All import { graph, Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups } from \"@pnp/graph/presets/all\"; Add a Group \u00b6 Add a new group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import { GroupType } from '@pnp/graph/groups'; const groupAddResult = await graph.groups.add(\"GroupName\", \"Mail_NickName\", GroupType.Office365); const group = await groupAddResult.group(); Delete a Group \u00b6 Deletes an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").delete(); Update Group Properties \u00b6 Updates an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").update({ displayName: newName, propertyName: updatedValue}); Add favorite \u00b6 Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").addFavorite(); Remove favorite \u00b6 Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").removeFavorite(); Reset Unseen Count \u00b6 Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").resetUnseenCount(); Subscribe By Mail \u00b6 Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").subscribeByMail(); Unsubscribe By Mail \u00b6 Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").unsubscribeByMail(); Get Calendar View \u00b6 Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; const startDate = new Date(\"2020-04-01\"); const endDate = new Date(\"2020-03-01\"); const events = graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").getCalendarView(startDate, endDate); Group Photo Operations \u00b6 See Photos Get the Team Site for a Group \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/sites/group\"; const teamSite = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").sites.root(); const url = teamSite.webUrl","title":"@pnp/graph/groups"},{"location":"v2/graph/groups/#pnpgraphgroups","text":"Groups are collections of users and other principals who share access to resources in Microsoft services or in your app. All group-related operations in Microsoft Graph require administrator consent. Note: Groups can only be created through work or school accounts. Personal Microsoft accounts don't support groups. You can learn more about Microsoft Graph Groups by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/groups"},{"location":"v2/graph/groups/#igroup-igroups","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups} from \"@pnp/graph/groups\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; Preset: All import { graph, Group, GroupType, Groups, IGroup, IGroupAddResult, IGroups } from \"@pnp/graph/presets/all\";","title":"IGroup, IGroups"},{"location":"v2/graph/groups/#add-a-group","text":"Add a new group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import { GroupType } from '@pnp/graph/groups'; const groupAddResult = await graph.groups.add(\"GroupName\", \"Mail_NickName\", GroupType.Office365); const group = await groupAddResult.group();","title":"Add a Group"},{"location":"v2/graph/groups/#delete-a-group","text":"Deletes an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").delete();","title":"Delete a Group"},{"location":"v2/graph/groups/#update-group-properties","text":"Updates an existing group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").update({ displayName: newName, propertyName: updatedValue});","title":"Update Group Properties"},{"location":"v2/graph/groups/#add-favorite","text":"Add the group to the list of the current user's favorite groups. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").addFavorite();","title":"Add favorite"},{"location":"v2/graph/groups/#remove-favorite","text":"Remove the group from the list of the current user's favorite groups. Supported for Office 365 Groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").removeFavorite();","title":"Remove favorite"},{"location":"v2/graph/groups/#reset-unseen-count","text":"Reset the unseenCount of all the posts that the current user has not seen since their last visit. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").resetUnseenCount();","title":"Reset Unseen Count"},{"location":"v2/graph/groups/#subscribe-by-mail","text":"Calling this method will enable the current user to receive email notifications for this group, about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").subscribeByMail();","title":"Subscribe By Mail"},{"location":"v2/graph/groups/#unsubscribe-by-mail","text":"Calling this method will prevent the current user from receiving email notifications for this group about new posts, events, and files in that group. Supported for Office 365 groups only. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").unsubscribeByMail();","title":"Unsubscribe By Mail"},{"location":"v2/graph/groups/#get-calendar-view","text":"Get the occurrences, exceptions, and single instances of events in a calendar view defined by a time range, from the default calendar of a group. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; const startDate = new Date(\"2020-04-01\"); const endDate = new Date(\"2020-03-01\"); const events = graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").getCalendarView(startDate, endDate);","title":"Get Calendar View"},{"location":"v2/graph/groups/#group-photo-operations","text":"See Photos","title":"Group Photo Operations"},{"location":"v2/graph/groups/#get-the-team-site-for-a-group","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/sites/group\"; const teamSite = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").sites.root(); const url = teamSite.webUrl","title":"Get the Team Site for a Group"},{"location":"v2/graph/insights/","text":"@pnp/graph/insights \u00b6 This module helps you get Insights in form of Trending , Used and Shared . The results are based on relationships calculated using advanced analytics and machine learning techniques. IInsights \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; Preset: All import \"@pnp/graph/presets/all\"; Get all Trending documents \u00b6 Returns documents from OneDrive and SharePoint sites trending around a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trending = await graph.me.insights.trending() const trending = await graph.users.getById(\"userId\").insights.trending() Get a Trending document by Id \u00b6 Using the getById method to get a trending document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trendingDoc = await graph.me.insights.trending.getById('Id')() const trendingDoc = await graph.users.getById(\"userId\").insights.trending.getById('Id')() Get the resource from Trending document \u00b6 Using the resources method to get the resource from a trending document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.trending.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.trending.getById('Id').resource() Get all Used documents \u00b6 Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const used = await graph.me.insights.used() const used = await graph.users.getById(\"userId\").insights.used() Get a Used document by Id \u00b6 Using the getById method to get a used document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const usedDoc = await graph.me.insights.used.getById('Id')() const usedDoc = await graph.users.getById(\"userId\").insights.used.getById('Id')() Get the resource from Used document \u00b6 Using the resources method to get the resource from a used document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.used.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.used.getById('Id').resource() Get all Shared documents \u00b6 Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const shared = await graph.me.insights.shared() const shared = await graph.users.getById(\"userId\").insights.shared() Get a Shared document by Id \u00b6 Using the getById method to get a shared document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const sharedDoc = await graph.me.insights.shared.getById('Id')() const sharedDoc = await graph.users.getById(\"userId\").insights.shared.getById('Id')() Get the resource from a Shared document \u00b6 Using the resources method to get the resource from a shared document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.shared.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.shared.getById('Id').resource()","title":"@pnp/graph/insights"},{"location":"v2/graph/insights/#pnpgraphinsights","text":"This module helps you get Insights in form of Trending , Used and Shared . The results are based on relationships calculated using advanced analytics and machine learning techniques.","title":"@pnp/graph/insights"},{"location":"v2/graph/insights/#iinsights","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInsights"},{"location":"v2/graph/insights/#get-all-trending-documents","text":"Returns documents from OneDrive and SharePoint sites trending around a user. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trending = await graph.me.insights.trending() const trending = await graph.users.getById(\"userId\").insights.trending()","title":"Get all Trending documents"},{"location":"v2/graph/insights/#get-a-trending-document-by-id","text":"Using the getById method to get a trending document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const trendingDoc = await graph.me.insights.trending.getById('Id')() const trendingDoc = await graph.users.getById(\"userId\").insights.trending.getById('Id')()","title":"Get a Trending document by Id"},{"location":"v2/graph/insights/#get-the-resource-from-trending-document","text":"Using the resources method to get the resource from a trending document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.trending.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.trending.getById('Id').resource()","title":"Get the resource from Trending document"},{"location":"v2/graph/insights/#get-all-used-documents","text":"Returns documents viewed and modified by a user. Includes documents the user used in OneDrive for Business, SharePoint, opened as email attachments, and as link attachments from sources like Box, DropBox and Google Drive. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const used = await graph.me.insights.used() const used = await graph.users.getById(\"userId\").insights.used()","title":"Get all Used documents"},{"location":"v2/graph/insights/#get-a-used-document-by-id","text":"Using the getById method to get a used document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const usedDoc = await graph.me.insights.used.getById('Id')() const usedDoc = await graph.users.getById(\"userId\").insights.used.getById('Id')()","title":"Get a Used document by Id"},{"location":"v2/graph/insights/#get-the-resource-from-used-document","text":"Using the resources method to get the resource from a used document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.used.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.used.getById('Id').resource()","title":"Get the resource from Used document"},{"location":"v2/graph/insights/#get-all-shared-documents","text":"Returns documents shared with a user. Documents can be shared as email attachments or as OneDrive for Business links sent in emails. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const shared = await graph.me.insights.shared() const shared = await graph.users.getById(\"userId\").insights.shared()","title":"Get all Shared documents"},{"location":"v2/graph/insights/#get-a-shared-document-by-id","text":"Using the getById method to get a shared document by Id. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const sharedDoc = await graph.me.insights.shared.getById('Id')() const sharedDoc = await graph.users.getById(\"userId\").insights.shared.getById('Id')()","title":"Get a Shared document by Id"},{"location":"v2/graph/insights/#get-the-resource-from-a-shared-document","text":"Using the resources method to get the resource from a shared document. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/insights\"; import \"@pnp/graph/users\"; const resource = await graph.me.insights.shared.getById('Id').resource() const resource = await graph.users.getById(\"userId\").insights.shared.getById('Id').resource()","title":"Get the resource from a Shared document"},{"location":"v2/graph/invitations/","text":"@pnp/graph/invitations \u00b6 The ability invite an external user via the invitation manager IInvitations \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\"; Preset: All import \"@pnp/graph/presets/all\"; Create Invitation \u00b6 Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\" const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');","title":"@pnp/graph/invitations"},{"location":"v2/graph/invitations/#pnpgraphinvitations","text":"The ability invite an external user via the invitation manager","title":"@pnp/graph/invitations"},{"location":"v2/graph/invitations/#iinvitations","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInvitations"},{"location":"v2/graph/invitations/#create-invitation","text":"Using the invitations.create() you can create an Invitation. We need the email address of the user being invited and the URL user should be redirected to once the invitation is redeemed (redirect URL). import { graph } from \"@pnp/graph\"; import \"@pnp/graph/invitations\" const invitationResult = await graph.invitations.create('external.user@email-address.com', 'https://tenant.sharepoint.com/sites/redirecturi');","title":"Create Invitation"},{"location":"v2/graph/onedrive/","text":"@pnp/graph/onedrive \u00b6 The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive. IInvitations \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; Preset: All import \"@pnp/graph/presets/all\"; Get the default drive \u00b6 Using the drive() you can get the default drive from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives(); Get all of the drives \u00b6 Using the drives() you can get the users available drives from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives(); Get drive by Id \u00b6 Using the drives.getById() you can get one of the available drives in Outlook import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId'); const drive = await graph.me.drives.getById('driveId'); Get the associated list of a drive \u00b6 Using the list() you get the associated list import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list(); const list = await graph.me.drives.getById('driveId').list(); Get the recent files \u00b6 Using the recent() you get the recent files import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent(); const files = await graph.me.drives.getById('driveId').recent(); Get the files shared with me \u00b6 Using the sharedWithMe() you get the files shared with the user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe(); const shared = await graph.me.drives.getById('driveId').sharedWithMe(); Get the Root folder \u00b6 Using the root() you get the root folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root(); const root = await graph.me.drives.getById('driveId').root(); Get the Children \u00b6 Using the children() you get the children import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children(); const rootChildren = await graph.me.drives.getById('driveId').root.children(); const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children(); const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children(); Add folder or item \u00b6 Using the add you can add a folder or an item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; import { DriveItem as IDriveItem } from \"@microsoft/microsoft-graph-types\"; const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', {folder: {}}); const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', {folder: {}}); Search items \u00b6 Using the search() you can search for items, and optionally select properties import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText')(); const search = await graph.me.drives.getById('driveId')root.search('queryText')(); Get specific item in drive \u00b6 Using the items.getById() you can get a specific item from the current drive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId'); const item = await graph.me.drives.getById('driveId').items.getById('itemId'); Get thumbnails \u00b6 Using the thumbnails() you get the thumbnails import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails(); Delete drive item \u00b6 Using the delete() you delete the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete(); Update drive item \u00b6 Using the update() you update the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"}); const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"}); Move drive item \u00b6 Using the move() you move the current item, and optionally update it import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; // Requires a parentReference to the new folder location const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"}); const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"});","title":"@pnp/graph/onedrive"},{"location":"v2/graph/onedrive/#pnpgraphonedrive","text":"The ability to manage drives and drive items in Onedrive is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can manage drives and drive items in Onedrive.","title":"@pnp/graph/onedrive"},{"location":"v2/graph/onedrive/#iinvitations","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInvitations"},{"location":"v2/graph/onedrive/#get-the-default-drive","text":"Using the drive() you can get the default drive from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives();","title":"Get the default drive"},{"location":"v2/graph/onedrive/#get-all-of-the-drives","text":"Using the drives() you can get the users available drives from Onedrive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drives = await graph.users.getById('user@tenant.onmicrosoft.com').drives(); const drives = await graph.me.drives();","title":"Get all of the drives"},{"location":"v2/graph/onedrive/#get-drive-by-id","text":"Using the drives.getById() you can get one of the available drives in Outlook import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const drive = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId'); const drive = await graph.me.drives.getById('driveId');","title":"Get drive by Id"},{"location":"v2/graph/onedrive/#get-the-associated-list-of-a-drive","text":"Using the list() you get the associated list import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const list = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').list(); const list = await graph.me.drives.getById('driveId').list();","title":"Get the associated list of a drive"},{"location":"v2/graph/onedrive/#get-the-recent-files","text":"Using the recent() you get the recent files import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const files = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').recent(); const files = await graph.me.drives.getById('driveId').recent();","title":"Get the recent files"},{"location":"v2/graph/onedrive/#get-the-files-shared-with-me","text":"Using the sharedWithMe() you get the files shared with the user import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const shared = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').sharedWithMe(); const shared = await graph.me.drives.getById('driveId').sharedWithMe();","title":"Get the files shared with me"},{"location":"v2/graph/onedrive/#get-the-root-folder","text":"Using the root() you get the root folder import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const root = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root(); const root = await graph.me.drives.getById('driveId').root();","title":"Get the Root folder"},{"location":"v2/graph/onedrive/#get-the-children","text":"Using the children() you get the children import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const rootChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children(); const rootChildren = await graph.me.drives.getById('driveId').root.children(); const itemChildren = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').children(); const itemChildren = await graph.me.drives.getById('driveId').root.items.getById('itemId').children();","title":"Get the Children"},{"location":"v2/graph/onedrive/#add-folder-or-item","text":"Using the add you can add a folder or an item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; import { DriveItem as IDriveItem } from \"@microsoft/microsoft-graph-types\"; const addFolder = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').root.children.add('New Folder', {folder: {}}); const addFolder = await graph.me.drives.getById('driveId').root.children.add('New Folder', {folder: {}});","title":"Add folder or item"},{"location":"v2/graph/onedrive/#search-items","text":"Using the search() you can search for items, and optionally select properties import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const search = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId')root.search('queryText')(); const search = await graph.me.drives.getById('driveId')root.search('queryText')();","title":"Search items"},{"location":"v2/graph/onedrive/#get-specific-item-in-drive","text":"Using the items.getById() you can get a specific item from the current drive import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const item = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId'); const item = await graph.me.drives.getById('driveId').items.getById('itemId');","title":"Get specific item in drive"},{"location":"v2/graph/onedrive/#get-thumbnails","text":"Using the thumbnails() you get the thumbnails import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').thumbnails(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').thumbnails();","title":"Get thumbnails"},{"location":"v2/graph/onedrive/#delete-drive-item","text":"Using the delete() you delete the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const thumbs = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').delete(); const thumbs = await graph.me.drives.getById('driveId').items.getById('itemId').delete();","title":"Delete drive item"},{"location":"v2/graph/onedrive/#update-drive-item","text":"Using the update() you update the current item import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; const update = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"}); const update = await graph.me.drives.getById('driveId').items.getById('itemId').update({name: \"New Name\"});","title":"Update drive item"},{"location":"v2/graph/onedrive/#move-drive-item","text":"Using the move() you move the current item, and optionally update it import { graph } from \"@pnp/graph\"; import \"@pnp/graph/onedrive\"; // Requires a parentReference to the new folder location const move = await graph.users.getById('user@tenant.onmicrosoft.com').drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"}); const move = await graph.me.drives.getById('driveId').items.getById('itemId').move({ parentReference: { id: 'itemId'}}, {name: \"New Name\"});","title":"Move drive item"},{"location":"v2/graph/outlook/","text":"@pnp/graph/outlook \u00b6 Represents the Outlook services available to a user. Currently, only interacting with categories is supported. You can learn more by reading the Official Microsoft Graph Documentation . IUsers, IUser, IPeople \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Outlook, IOutlook, MasterCategories, IMasterCategories, OutlookCategory, IOutlookCategory} from \"@pnp/graph/outlook\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/outlook\"; Preset: All import { graph, Outlook, IOutlook, MasterCategories, IMasterCategories } from \"@pnp/graph/presets/all\"; Get All Categories User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories(); Add Category User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions await graph.me.outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' }); // Application permissions await graph.users.getById('{user id}').outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' }); Update Category \u00b6 Testing has shown that displayName cannot be updated. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; import { OutlookCategory } from \"@microsoft/microsoft-graph-types\"; const categoryUpdate: OutlookCategory = { color: \"preset5\" } // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate); Delete Category \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();","title":"@pnp/graph/outlook"},{"location":"v2/graph/outlook/#pnpgraphoutlook","text":"Represents the Outlook services available to a user. Currently, only interacting with categories is supported. You can learn more by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/outlook"},{"location":"v2/graph/outlook/#iusers-iuser-ipeople","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {Outlook, IOutlook, MasterCategories, IMasterCategories, OutlookCategory, IOutlookCategory} from \"@pnp/graph/outlook\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/outlook\"; Preset: All import { graph, Outlook, IOutlook, MasterCategories, IMasterCategories } from \"@pnp/graph/presets/all\";","title":"IUsers, IUser, IPeople"},{"location":"v2/graph/outlook/#get-all-categories-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories();","title":"Get All Categories User"},{"location":"v2/graph/outlook/#add-category-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions await graph.me.outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' }); // Application permissions await graph.users.getById('{user id}').outlook.masterCategories.add({ displayName: 'Newsletters', color: 'preset2' });","title":"Add Category User"},{"location":"v2/graph/outlook/#update-category","text":"Testing has shown that displayName cannot be updated. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; import { OutlookCategory } from \"@microsoft/microsoft-graph-types\"; const categoryUpdate: OutlookCategory = { color: \"preset5\" } // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').update(categoryUpdate); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').update(categoryUpdate);","title":"Update Category"},{"location":"v2/graph/outlook/#delete-category","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/outlook\"; // Delegated permissions const categories = await graph.me.outlook.masterCategories.getById('{category id}').delete(); // Application permissions const categories = await graph.users.getById('{user id}').outlook.masterCategories.getById('{category id}').delete();","title":"Delete Category"},{"location":"v2/graph/photos/","text":"@pnp/graph/photos \u00b6 A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation . IPhoto \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IPhoto, Photo} from \"@pnp/graph/photos\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/photos\"; Preset: All import { graph, IPhoto, Photo } from \"@pnp/sp/presets/all\"; Current User Photo \u00b6 This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const photoValue = await graph.me.photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl); Current Group Photo \u00b6 This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/photos\"; const photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl); Set User Photo \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file); Set Group Photo \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file);","title":"@pnp/graph/photos"},{"location":"v2/graph/photos/#pnpgraphphotos","text":"A profile photo of a user, group or an Outlook contact accessed from Exchange Online or Azure Active Directory (AAD). It's binary data not encoded in base-64. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/photos"},{"location":"v2/graph/photos/#iphoto","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IPhoto, Photo} from \"@pnp/graph/photos\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/photos\"; Preset: All import { graph, IPhoto, Photo } from \"@pnp/sp/presets/all\";","title":"IPhoto"},{"location":"v2/graph/photos/#current-user-photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const photoValue = await graph.me.photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);","title":"Current User Photo"},{"location":"v2/graph/photos/#current-group-photo","text":"This example shows the getBlob() endpoint, there is also a getBuffer() endpoint to support node.js import { graph } from \"@pnp/graph\"; import \"@pnp/graph/groups\"; import \"@pnp/graph/photos\"; const photoValue = await graph.groups.getById(\"7d2b9355-0891-47d3-84c8-bf2cd9c62177\").photo.getBlob(); const url = window.URL || window.webkitURL; const blobUrl = url.createObjectURL(photoValue); document.getElementById(\"photoElement\").setAttribute(\"src\", blobUrl);","title":"Current Group Photo"},{"location":"v2/graph/photos/#set-user-photo","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file);","title":"Set User Photo"},{"location":"v2/graph/photos/#set-group-photo","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const input = document.getElementById(\"thefileinput\"); const file = input.files[0]; await graph.me.photo.setContent(file);","title":"Set Group Photo"},{"location":"v2/graph/planner/","text":"@pnp/graph/planner \u00b6 The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner. IInvitations \u00b6 Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\"; Preset: All import \"@pnp/graph/presets/all\"; Get Plans by Id \u00b6 Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const plan = await graph.planner.plans.getById('planId')(); Add new Plan \u00b6 Using the planner.plans.add() you can create a new Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newPlan = await graph.planner.plans.add('groupObjectId', 'title'); Get Tasks in Plan \u00b6 Using the tasks() you can get the Tasks in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planTasks = await graph.planner.plans.getById('planId').tasks(); Get Buckets in Plan \u00b6 Using the buckets() you can get the Buckets in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planBuckets = await graph.planner.plans.getById('planId').buckets(); Get Details in Plan \u00b6 Using the details() you can get the details in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planDetails = await graph.planner.plans.getById('planId').details(); Delete Plan \u00b6 Using the delete() you can get delete a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delPlan = await graph.planner.plans.getById('planId').delete('planEtag'); Update Plan \u00b6 Using the update() you can get update a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'}); Get Task by Id \u00b6 Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const task = await graph.planner.tasks.getById('taskId')(); Add new Task \u00b6 Using the planner.tasks.add() you can create a new Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newTask = await graph.planner.tasks.add('planId', 'title'); Get Details in Task \u00b6 Using the details() you can get the details in a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const taskDetails = await graph.planner.tasks.getById('taskId').details(); Delete Task \u00b6 Using the delete() you can get delete a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag'); Update Task \u00b6 Using the update() you can get update a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'}); Get Buckets by Id \u00b6 Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucket = await graph.planner.buckets.getById('bucketId')(); Add new Bucket \u00b6 Using the planner.buckets.add() you can create a new Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newBucket = await graph.planner.buckets.add('name', 'planId'); Update Bucket \u00b6 Using the update() you can get update a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updBucket = await graph.planner.buckets.getById('bucketId').update({name: \"Name\", eTag:'bucketEtag'}); Delete Bucket \u00b6 Using the delete() you can get delete a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag'); Get Bucket Tasks \u00b6 Using the tasks() you can get Tasks in a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();","title":"@pnp/graph/planner"},{"location":"v2/graph/planner/#pnpgraphplanner","text":"The ability to manage plans and tasks in Planner is a capability introduced in version 1.2.4 of @pnp/graph. Through the methods described you can add, update and delete items in Planner.","title":"@pnp/graph/planner"},{"location":"v2/graph/planner/#iinvitations","text":"Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\"; Preset: All import \"@pnp/graph/presets/all\";","title":"IInvitations"},{"location":"v2/graph/planner/#get-plans-by-id","text":"Using the planner.plans.getById() you can get a specific Plan. Planner.plans is not an available endpoint, you need to get a specific Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const plan = await graph.planner.plans.getById('planId')();","title":"Get Plans by Id"},{"location":"v2/graph/planner/#add-new-plan","text":"Using the planner.plans.add() you can create a new Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newPlan = await graph.planner.plans.add('groupObjectId', 'title');","title":"Add new Plan"},{"location":"v2/graph/planner/#get-tasks-in-plan","text":"Using the tasks() you can get the Tasks in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planTasks = await graph.planner.plans.getById('planId').tasks();","title":"Get Tasks in Plan"},{"location":"v2/graph/planner/#get-buckets-in-plan","text":"Using the buckets() you can get the Buckets in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planBuckets = await graph.planner.plans.getById('planId').buckets();","title":"Get Buckets in Plan"},{"location":"v2/graph/planner/#get-details-in-plan","text":"Using the details() you can get the details in a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const planDetails = await graph.planner.plans.getById('planId').details();","title":"Get Details in Plan"},{"location":"v2/graph/planner/#delete-plan","text":"Using the delete() you can get delete a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delPlan = await graph.planner.plans.getById('planId').delete('planEtag');","title":"Delete Plan"},{"location":"v2/graph/planner/#update-plan","text":"Using the update() you can get update a Plan. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updPlan = await graph.planner.plans.getById('planId').update({title: 'New Title', eTag: 'planEtag'});","title":"Update Plan"},{"location":"v2/graph/planner/#get-task-by-id","text":"Using the planner.tasks.getById() you can get a specific Task. Planner.tasks is not an available endpoint, you need to get a specific Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const task = await graph.planner.tasks.getById('taskId')();","title":"Get Task by Id"},{"location":"v2/graph/planner/#add-new-task","text":"Using the planner.tasks.add() you can create a new Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newTask = await graph.planner.tasks.add('planId', 'title');","title":"Add new Task"},{"location":"v2/graph/planner/#get-details-in-task","text":"Using the details() you can get the details in a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const taskDetails = await graph.planner.tasks.getById('taskId').details();","title":"Get Details in Task"},{"location":"v2/graph/planner/#delete-task","text":"Using the delete() you can get delete a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delTask = await graph.planner.tasks.getById('taskId').delete('taskEtag');","title":"Delete Task"},{"location":"v2/graph/planner/#update-task","text":"Using the update() you can get update a Task. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updTask = await graph.planner.tasks.getById('taskId').update({properties, eTag:'taskEtag'});","title":"Update Task"},{"location":"v2/graph/planner/#get-buckets-by-id","text":"Using the planner.buckets.getById() you can get a specific Bucket. planner.buckets is not an available endpoint, you need to get a specific Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucket = await graph.planner.buckets.getById('bucketId')();","title":"Get Buckets by Id"},{"location":"v2/graph/planner/#add-new-bucket","text":"Using the planner.buckets.add() you can create a new Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const newBucket = await graph.planner.buckets.add('name', 'planId');","title":"Add new Bucket"},{"location":"v2/graph/planner/#update-bucket","text":"Using the update() you can get update a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const updBucket = await graph.planner.buckets.getById('bucketId').update({name: \"Name\", eTag:'bucketEtag'});","title":"Update Bucket"},{"location":"v2/graph/planner/#delete-bucket","text":"Using the delete() you can get delete a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const delBucket = await graph.planner.buckets.getById('bucketId').delete(eTag:'bucketEtag');","title":"Delete Bucket"},{"location":"v2/graph/planner/#get-bucket-tasks","text":"Using the tasks() you can get Tasks in a Bucket. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/planner\" const bucketTasks = await graph.planner.buckets.getById('bucketId').tasks();","title":"Get Bucket Tasks"},{"location":"v2/graph/search/","text":"@pnp/graph/search \u00b6 The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below. Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; Preset: All import \"@pnp/graph/presets/all\"; Call graph.query \u00b6 This example shows calling the search API via the query method of the root graph object. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; const results = await graph.query({ entityTypes: [\"site\"], query: { queryString: \"test\" }, }); Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.","title":"@pnp/graph/search"},{"location":"v2/graph/search/#pnpgraphsearch","text":"The search module allows you to access the Microsoft Graph Search API. You can read full details of using the API, for library examples please see below. Scenario Import Statement Selective import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; Preset: All import \"@pnp/graph/presets/all\";","title":"@pnp/graph/search"},{"location":"v2/graph/search/#call-graphquery","text":"This example shows calling the search API via the query method of the root graph object. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/search\"; const results = await graph.query({ entityTypes: [\"site\"], query: { queryString: \"test\" }, }); Note: This library allows you to pass multiple search requests to the query method as the value consumed by the server is an array, but it only a single requests works at this time. Eventually this may change and no updates will be required.","title":"Call graph.query"},{"location":"v2/graph/subscriptions/","text":"@pnp/graph/subscriptions \u00b6 The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: Mail, events, and contacts from Outlook. Conversations from Office Groups. Drive root items from OneDrive. Users and Groups from Azure Active Directory. Alerts from the Microsoft Graph Security API. Get all of the Subscriptions \u00b6 Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscriptions = await graph.subscriptions(); Create a new Subscription \u00b6 Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const addedSubscription = await graph.subscriptions.add(\"created,updated\", \"https://webhook.azurewebsites.net/api/send/myNotifyClient\", \"me/mailFolders('Inbox')/messages\", \"2019-11-20T18:23:45.9356913Z\"); Get Subscription by Id \u00b6 Using the subscriptions.getById() you can get one of the subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscription = await graph.subscriptions.getById('subscriptionId')(); Delete a Subscription \u00b6 Using the subscriptions.getById().delete() you can remove one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const delSubscription = await graph.subscriptions.getById('subscriptionId').delete(); Update a Subscription \u00b6 Using the subscriptions.getById().update() you can update one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: \"created,updated,deleted\" });","title":"@pnp/graph/subscriptions"},{"location":"v2/graph/subscriptions/#pnpgraphsubscriptions","text":"The ability to manage subscriptions is a capability introduced in version 1.2.9 of @pnp/graph. A subscription allows a client app to receive notifications about changes to data in Microsoft Graph. Currently, subscriptions are enabled for the following resources: Mail, events, and contacts from Outlook. Conversations from Office Groups. Drive root items from OneDrive. Users and Groups from Azure Active Directory. Alerts from the Microsoft Graph Security API.","title":"@pnp/graph/subscriptions"},{"location":"v2/graph/subscriptions/#get-all-of-the-subscriptions","text":"Using the subscriptions(). If successful this method returns a 200 OK response code and a list of subscription objects in the response body. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscriptions = await graph.subscriptions();","title":"Get all of the Subscriptions"},{"location":"v2/graph/subscriptions/#create-a-new-subscription","text":"Using the subscriptions.add(). Creating a subscription requires read scope to the resource. For example, to get notifications messages, your app needs the Mail.Read permission. To learn more about the scopes visit this url. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const addedSubscription = await graph.subscriptions.add(\"created,updated\", \"https://webhook.azurewebsites.net/api/send/myNotifyClient\", \"me/mailFolders('Inbox')/messages\", \"2019-11-20T18:23:45.9356913Z\");","title":"Create a new Subscription"},{"location":"v2/graph/subscriptions/#get-subscription-by-id","text":"Using the subscriptions.getById() you can get one of the subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const subscription = await graph.subscriptions.getById('subscriptionId')();","title":"Get Subscription by Id"},{"location":"v2/graph/subscriptions/#delete-a-subscription","text":"Using the subscriptions.getById().delete() you can remove one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const delSubscription = await graph.subscriptions.getById('subscriptionId').delete();","title":"Delete a Subscription"},{"location":"v2/graph/subscriptions/#update-a-subscription","text":"Using the subscriptions.getById().update() you can update one of the Subscriptions import { graph } from \"@pnp/graph\"; import \"@pnp/graph/subscriptions\" const updSubscription = await graph.subscriptions.getById('subscriptionId').update({changeType: \"created,updated,deleted\" });","title":"Update a Subscription"},{"location":"v2/graph/teams/","text":"@pnp/graph/teams \u00b6 The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams. Teams the user is a member of \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/teams\" const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams(); const myJoinedTeams = await graph.me.joinedTeams(); Get Teams by Id \u00b6 Using the teams.getById() you can get a specific Team. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')(); Create new Team/Group - Method #1 \u00b6 The first way to create a new Team and corresponding Group is to first create the group and then create the team. Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group. Create a Team via a specific group \u00b6 Here we get the group via id and use createTeam import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" import \"@pnp/graph/groups\" const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ \"memberSettings\": { \"allowCreateUpdateChannels\": true }, \"messagingSettings\": { \"allowUserEditMessages\": true, \"allowUserDeleteMessages\": true }, \"funSettings\": { \"allowGiphy\": true, \"giphyContentRating\": \"strict\" }}); Create new Team/Group - Method #2 \u00b6 The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = { \"template@odata.bind\": \"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\", \"displayName\": \"PnPJS Test Team\", \"description\": \"PnPJS Test Team\u2019s Description\", \"members\": [ { \"@odata.type\": \"#microsoft.graph.aadUserConversationMember\", \"roles\": [\"owner\"], \"user@odata.bind\": \"https://graph.microsoft.com/v1.0/users('{owners user id}')\", }, ], }; const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team); //To check the status of the team creation, call getOperationById for the newly created team. const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId); Clone a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public'); Get Teams Async Operation \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public'); const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId); Archive a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive(); Unarchive a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive(); Get all channels of a Team \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels(); Get channel by Id \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')(); Create a new Channel \u00b6 import { graph } from \"@pnp/graph\"; const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description'); Get installed Apps \u00b6 import { graph } from \"@pnp/graph\"; const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps(); Add an App \u00b6 import { graph } from \"@pnp/graph\"; const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a'); Remove an App \u00b6 import { graph } from \"@pnp/graph\"; const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove(); Get Tabs from a Channel \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs(); Get Tab by Id \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')(); Add a new Tab \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',{});","title":"@pnp/graph/teams"},{"location":"v2/graph/teams/#pnpgraphteams","text":"The ability to manage Team is a capability introduced in the 1.2.7 of @pnp/graph. Through the methods described you can add, update and delete items in Teams.","title":"@pnp/graph/teams"},{"location":"v2/graph/teams/#teams-the-user-is-a-member-of","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\" import \"@pnp/graph/teams\" const joinedTeams = await graph.users.getById('99dc1039-eb80-43b1-a09e-250d50a80b26').joinedTeams(); const myJoinedTeams = await graph.me.joinedTeams();","title":"Teams the user is a member of"},{"location":"v2/graph/teams/#get-teams-by-id","text":"Using the teams.getById() you can get a specific Team. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528')();","title":"Get Teams by Id"},{"location":"v2/graph/teams/#create-new-teamgroup-method-1","text":"The first way to create a new Team and corresponding Group is to first create the group and then create the team. Follow the example in Groups to create the group and get the GroupID. Then make a call to create the team from the group.","title":"Create new Team/Group - Method #1"},{"location":"v2/graph/teams/#create-a-team-via-a-specific-group","text":"Here we get the group via id and use createTeam import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" import \"@pnp/graph/groups\" const createdTeam = await graph.groups.getById('679c8ff4-f07d-40de-b02b-60ec332472dd').createTeam({ \"memberSettings\": { \"allowCreateUpdateChannels\": true }, \"messagingSettings\": { \"allowUserEditMessages\": true, \"allowUserDeleteMessages\": true }, \"funSettings\": { \"allowGiphy\": true, \"giphyContentRating\": \"strict\" }});","title":"Create a Team via a specific group"},{"location":"v2/graph/teams/#create-new-teamgroup-method-2","text":"The second way to create a new Team and corresponding Group is to do so in one call. This can be done by using the createTeam method. import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const team = { \"template@odata.bind\": \"https://graph.microsoft.com/v1.0/teamsTemplates('standard')\", \"displayName\": \"PnPJS Test Team\", \"description\": \"PnPJS Test Team\u2019s Description\", \"members\": [ { \"@odata.type\": \"#microsoft.graph.aadUserConversationMember\", \"roles\": [\"owner\"], \"user@odata.bind\": \"https://graph.microsoft.com/v1.0/users('{owners user id}')\", }, ], }; const createdTeam: ITeamCreateResultAsync = await graph.teams.create(team); //To check the status of the team creation, call getOperationById for the newly created team. const createdTeamStatus = await graph.teams.getById(createdTeam.teamId).getOperationById(createdTeam.operationId);","title":"Create new Team/Group - Method #2"},{"location":"v2/graph/teams/#clone-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public');","title":"Clone a Team"},{"location":"v2/graph/teams/#get-teams-async-operation","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const clonedTeam = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').cloneTeam( 'Cloned','description','apps,tabs,settings,channels,members','public'); const clonedTeamStatus = await graph.teams.getById(clonedTeam.teamId).getOperationById(clonedTeam.operationId);","title":"Get Teams Async Operation"},{"location":"v2/graph/teams/#archive-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').archive();","title":"Archive a Team"},{"location":"v2/graph/teams/#unarchive-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const archived = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').unarchive();","title":"Unarchive a Team"},{"location":"v2/graph/teams/#get-all-channels-of-a-team","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channels = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels();","title":"Get all channels of a Team"},{"location":"v2/graph/teams/#get-channel-by-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const channel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype')();","title":"Get channel by Id"},{"location":"v2/graph/teams/#create-a-new-channel","text":"import { graph } from \"@pnp/graph\"; const newChannel = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').channels.create('New Channel', 'Description');","title":"Create a new Channel"},{"location":"v2/graph/teams/#get-installed-apps","text":"import { graph } from \"@pnp/graph\"; const installedApps = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps();","title":"Get installed Apps"},{"location":"v2/graph/teams/#add-an-app","text":"import { graph } from \"@pnp/graph\"; const addedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.add('https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a');","title":"Add an App"},{"location":"v2/graph/teams/#remove-an-app","text":"import { graph } from \"@pnp/graph\"; const removedApp = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528').installedApps.remove();","title":"Remove an App"},{"location":"v2/graph/teams/#get-tabs-from-a-channel","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tabs = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs();","title":"Get Tabs from a Channel"},{"location":"v2/graph/teams/#get-tab-by-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const tab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.getById('Id')();","title":"Get Tab by Id"},{"location":"v2/graph/teams/#add-a-new-tab","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/teams\" const newTab = await graph.teams.getById('3531f3fb-f9ee-4f43-982a-6c90d8226528'). channels.getById('19:65723d632b384ca89c81115c281428a3@thread.skype').tabs.add('Tab','https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/12345678-9abc-def0-123456789a',{});","title":"Add a new Tab"},{"location":"v2/graph/users/","text":"@pnp/graph/users \u00b6 Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation . IUsers, IUser, IPeople \u00b6 Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IUser, IUsers, User, Users, IPeople, People} from \"@pnp/graph/users\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; Preset: All import { graph,IUser, IUsers, User, Users, IPeople, People } from \"@pnp/graph/presets/all\"; Current User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const currentUser = await graph.me(); Get All Users in the Organization \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const allUsers = await graph.users(); Get a User by email address (or user id) \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const matchingUser = await graph.users.getById('jane@contoso.com')(); Update Current User \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; await graph.me.update({ displayName: 'John Doe' }); People \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)(); People \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)(); Manager \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const manager = await graph.me.manager(); Direct Reports \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const reports = await graph.me.directReports(); Photo \u00b6 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const currentUser = await graph.me.photo(); const specificUser = await graph.users.getById('jane@contoso.com').photo(); User Photo Operations \u00b6 See Photos","title":"@pnp/graph/users"},{"location":"v2/graph/users/#pnpgraphusers","text":"Users are Azure Active Directory objects representing users in the organizations. They represent the single identity for a person across Microsoft 365 services. You can learn more about Microsoft Graph users by reading the Official Microsoft Graph Documentation .","title":"@pnp/graph/users"},{"location":"v2/graph/users/#iusers-iuser-ipeople","text":"Scenario Import Statement Selective 1 import { graph } from \"@pnp/graph\"; import {IUser, IUsers, User, Users, IPeople, People} from \"@pnp/graph/users\"; Selective 2 import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; Preset: All import { graph,IUser, IUsers, User, Users, IPeople, People } from \"@pnp/graph/presets/all\";","title":"IUsers, IUser, IPeople"},{"location":"v2/graph/users/#current-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const currentUser = await graph.me();","title":"Current User"},{"location":"v2/graph/users/#get-all-users-in-the-organization","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const allUsers = await graph.users();","title":"Get All Users in the Organization"},{"location":"v2/graph/users/#get-a-user-by-email-address-or-user-id","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const matchingUser = await graph.users.getById('jane@contoso.com')();","title":"Get a User by email address (or user id)"},{"location":"v2/graph/users/#update-current-user","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; await graph.me.update({ displayName: 'John Doe' });","title":"Update Current User"},{"location":"v2/graph/users/#people","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)();","title":"People"},{"location":"v2/graph/users/#people_1","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const people = await graph.me.people(); // get the top 3 people const people = await graph.me.people.top(3)();","title":"People"},{"location":"v2/graph/users/#manager","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const manager = await graph.me.manager();","title":"Manager"},{"location":"v2/graph/users/#direct-reports","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; const reports = await graph.me.directReports();","title":"Direct Reports"},{"location":"v2/graph/users/#photo","text":"import { graph } from \"@pnp/graph\"; import \"@pnp/graph/users\"; import \"@pnp/graph/photos\"; const currentUser = await graph.me.photo(); const specificUser = await graph.users.getById('jane@contoso.com').photo();","title":"Photo"},{"location":"v2/graph/users/#user-photo-operations","text":"See Photos","title":"User Photo Operations"},{"location":"v2/logging/","text":"@pnp/logging \u00b6 The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers. Getting Started \u00b6 Install the logging module, it has no other dependencies npm install @pnp/logging --save Understanding the Logging Framework \u00b6 The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter. /** * Interface that defines a log listener * */ export interface ILogListener { /** * Any associated data that a given logging listener may choose to log or ignore * * @param entry The information to be logged */ log(entry: ILogEntry): void; } /** * Interface that defines a log entry * */ export interface ILogEntry { /** * The main message to be logged */ message: string; /** * The level of information this message represents */ level: LogLevel; /** * Any associated data that a given logging listener may choose to log or ignore */ data?: any; } Log Levels \u00b6 export const enum LogLevel { Verbose = 0, Info = 1, Warning = 2, Error = 3, Off = 99, } Writing to the Logger \u00b6 To write information to a logger you can use either write, writeJSON, or log. import { Logger, LogLevel } from \"@pnp/logging\"; // write logs a simple string as the message value of the LogEntry Logger.write(\"This is logging a simple string\"); // optionally passing a level, default level is Verbose Logger.write(\"This is logging a simple string\", LogLevel.Error); // this will convert the object to a string using JSON.stringify and set the message with the result Logger.writeJSON({ name: \"value\", name2: \"value2\"}); // optionally passing a level, default level is Verbose Logger.writeJSON({ name: \"value\", name2: \"value2\"}, LogLevel.Warning); // specify the entire LogEntry interface using log Logger.log({ data: { name: \"value\", name2: \"value2\"}, level: LogLevel.Warning, message: \"This is my message\" }); Log an error \u00b6 There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance passed in, the level will be 'Error', and the message will be the Error instance's message property. const e = Error(\"An Error\"); Logger.error(e); Subscribing a Listener \u00b6 By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; // subscribe a listener Logger.subscribe(new ConsoleListener()); // set the active log level Logger.activeLogLevel = LogLevel.Info; Available Listeners \u00b6 There are two listeners included in the library, ConsoleListener and FunctionListener. ConsoleListener \u00b6 This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above. Configuration Options \u00b6 Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel). Using a Prefix \u00b6 To add a prefix to all output, supply a string in the constructor: import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE)); Logger.activeLogLevel = LogLevel.Info; With the above configuration, Logger.write(\"My special message\"); will be output to the console as: MyAwesomeWebPart - My special message Customizing Text Color \u00b6 You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color). Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.): import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'})); Logger.activeLogLevel = LogLevel.Info; With the above configuration: Logger.write(\"My special message\"); Logger.write(\"A warning!\", LogLevel.Warning); Will result in messages that look like this: Color options: color : Default text color for all logging levels unless they're specified verboseColor : Text color to use for messages with LogLevel.Verbose infoColor : Text color to use for messages with LogLevel.Info warningColor : Text color to use for messages with LogLevel.Warning errorColor : Text color to use for messages with LogLevel.Error To set colors without a prefix, specify either undefined or an empty string for the first parameter: Logger.subscribe(new ConsoleListener(undefined, {color:'purple'})); FunctionListener \u00b6 The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. import { Logger, FunctionListener, ILogEntry } from \"@pnp/logging\"; let listener = new FunctionListener((entry: ILogEntry) => { // pass all logging data to an existing framework MyExistingCompanyLoggingFramework.log(entry.message); }); Logger.subscribe(listener); Create a Custom Listener \u00b6 If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface. import { Logger, ILogListener, ILogEntry } from \"@pnp/logging\"; class MyListener implements ILogListener { log(entry: ILogEntry): void { // here you would do something with the entry } } Logger.subscribe(new MyListener());","title":"@pnp/logging"},{"location":"v2/logging/#pnplogging","text":"The logging module provides light weight subscribable and extensible logging framework which is used internally and available for use in your projects. This article outlines how to setup logging and use the various loggers.","title":"@pnp/logging"},{"location":"v2/logging/#getting-started","text":"Install the logging module, it has no other dependencies npm install @pnp/logging --save","title":"Getting Started"},{"location":"v2/logging/#understanding-the-logging-framework","text":"The logging framework is centered on the Logger class to which any number of listeners can be subscribed. Each of these listeners will receive each of the messages logged. Each listener must implement the ILogListener interface, shown below. There is only one method to implement and it takes an instance of the LogEntry interface as a parameter. /** * Interface that defines a log listener * */ export interface ILogListener { /** * Any associated data that a given logging listener may choose to log or ignore * * @param entry The information to be logged */ log(entry: ILogEntry): void; } /** * Interface that defines a log entry * */ export interface ILogEntry { /** * The main message to be logged */ message: string; /** * The level of information this message represents */ level: LogLevel; /** * Any associated data that a given logging listener may choose to log or ignore */ data?: any; }","title":"Understanding the Logging Framework"},{"location":"v2/logging/#log-levels","text":"export const enum LogLevel { Verbose = 0, Info = 1, Warning = 2, Error = 3, Off = 99, }","title":"Log Levels"},{"location":"v2/logging/#writing-to-the-logger","text":"To write information to a logger you can use either write, writeJSON, or log. import { Logger, LogLevel } from \"@pnp/logging\"; // write logs a simple string as the message value of the LogEntry Logger.write(\"This is logging a simple string\"); // optionally passing a level, default level is Verbose Logger.write(\"This is logging a simple string\", LogLevel.Error); // this will convert the object to a string using JSON.stringify and set the message with the result Logger.writeJSON({ name: \"value\", name2: \"value2\"}); // optionally passing a level, default level is Verbose Logger.writeJSON({ name: \"value\", name2: \"value2\"}, LogLevel.Warning); // specify the entire LogEntry interface using log Logger.log({ data: { name: \"value\", name2: \"value2\"}, level: LogLevel.Warning, message: \"This is my message\" });","title":"Writing to the Logger"},{"location":"v2/logging/#log-an-error","text":"There exists a shortcut method to log an error to the Logger. This will log an entry to the subscribed loggers where the data property will be the Error instance passed in, the level will be 'Error', and the message will be the Error instance's message property. const e = Error(\"An Error\"); Logger.error(e);","title":"Log an error"},{"location":"v2/logging/#subscribing-a-listener","text":"By default no listeners are subscribed, so if you would like to get logging information you need to subscribe at least one listener. This is done as shown below by importing the Logger and your listener(s) of choice. Here we are using the provided ConsoleListener. We are also setting the active log level, which controls the level of logging that will be output. Be aware that Verbose produces a substantial amount of data about each request. import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; // subscribe a listener Logger.subscribe(new ConsoleListener()); // set the active log level Logger.activeLogLevel = LogLevel.Info;","title":"Subscribing a Listener"},{"location":"v2/logging/#available-listeners","text":"There are two listeners included in the library, ConsoleListener and FunctionListener.","title":"Available Listeners"},{"location":"v2/logging/#consolelistener","text":"This listener outputs information to the console and works in Node as well as within browsers. It can be used without settings and writes to the appropriate console method based on message level. For example a LogEntry with level Warning will be written to console.warn. Basic usage is shown in the example above.","title":"ConsoleListener"},{"location":"v2/logging/#configuration-options","text":"Although ConsoleListener can be used without configuration, there are some additional options available to you. ConsoleListener supports adding a prefix to every output (helpful for filtering console messages) and specifying text color for messages (including by LogLevel).","title":"Configuration Options"},{"location":"v2/logging/#using-a-prefix","text":"To add a prefix to all output, supply a string in the constructor: import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE)); Logger.activeLogLevel = LogLevel.Info; With the above configuration, Logger.write(\"My special message\"); will be output to the console as: MyAwesomeWebPart - My special message","title":"Using a Prefix"},{"location":"v2/logging/#customizing-text-color","text":"You can also specify text color for your messages by supplying an IConsoleListenerColors object. You can simply specify color to set the default color for all logging levels or you can set one or more logging level specific text colors (if you only want to set color for a specific logging level(s), leave color out and all other log levels will use the default color). Colors can be specified the same way color values are specified in CSS (named colors, hex values, rgb, rgba, hsl, hsla, etc.): import { Logger, ConsoleListener, LogLevel } from \"@pnp/logging\"; const LOG_SOURCE: string = 'MyAwesomeWebPart'; Logger.subscribe(new ConsoleListener(LOG_SOURCE, {color:'#0b6a0b',warningColor:'magenta'})); Logger.activeLogLevel = LogLevel.Info; With the above configuration: Logger.write(\"My special message\"); Logger.write(\"A warning!\", LogLevel.Warning); Will result in messages that look like this: Color options: color : Default text color for all logging levels unless they're specified verboseColor : Text color to use for messages with LogLevel.Verbose infoColor : Text color to use for messages with LogLevel.Info warningColor : Text color to use for messages with LogLevel.Warning errorColor : Text color to use for messages with LogLevel.Error To set colors without a prefix, specify either undefined or an empty string for the first parameter: Logger.subscribe(new ConsoleListener(undefined, {color:'purple'}));","title":"Customizing Text Color"},{"location":"v2/logging/#functionlistener","text":"The FunctionListener allows you to wrap any functionality by creating a function that takes a LogEntry as its single argument. This produces the same result as implementing the LogListener interface, but is useful if you already have a logging method or framework to which you want to pass the messages. import { Logger, FunctionListener, ILogEntry } from \"@pnp/logging\"; let listener = new FunctionListener((entry: ILogEntry) => { // pass all logging data to an existing framework MyExistingCompanyLoggingFramework.log(entry.message); }); Logger.subscribe(listener);","title":"FunctionListener"},{"location":"v2/logging/#create-a-custom-listener","text":"If desirable for your project you can create a custom listener to perform any logging action you would like. This is done by implementing the ILogListener interface. import { Logger, ILogListener, ILogEntry } from \"@pnp/logging\"; class MyListener implements ILogListener { log(entry: ILogEntry): void { // here you would do something with the entry } } Logger.subscribe(new MyListener());","title":"Create a Custom Listener"},{"location":"v2/news/2020-year-in-review/","text":"2020 Year End Report \u00b6 Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year. This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules. We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community. Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured \"roots\" such as \"sp\" or \"graph\" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios. Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience. Usage \u00b6 In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227. These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November. 1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds Releases \u00b6 We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log , updated with each release. You can check our scheduled releases through project milestones , understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries. NPM Package download statistics (@pnp/sp): \u00b6 Month Count * Month Count January 100,686 * July 36,805 February 34,437 * August 38,897 March 34,574 * September 45,968 April 32,436 * October 46,655 May 34,482 * November 45,511 June 34,408 * December 58,977 Grand Total 543,836 With 2020 our total all time downloads of @pnp/sp is now at: 949,638 Stats from https://npm-stat.com/ Future Plans \u00b6 Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date. Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements. New Lead Maintainer \u00b6 With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner ! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work. Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean. We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come. Contributors \u00b6 As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started. Sponsors \u00b6 We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs. Thank You Closing \u00b6 In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program. Wishing you the very best for 2021, The PnPjs Team","title":"2020 Year End Report"},{"location":"v2/news/2020-year-in-review/#2020-year-end-report","text":"Welcome to our first year in review report for PnPjs. This year has marked usage milestones, seen more contributors than ever, and expanded the core maintainers team. But none of this would be possible without everyones support and participation - so we start by saying Thank You! We deeply appreciate everyone that has used, helped us grow, and improved the library over the last year. This year we introduced MSAL clients for node and browser, improved our testing/local development plumbing, and updated the libraries to work with the node 15 module resolution rules. We fixed 43 reported bugs, answered 131 questions, and made 55 suggested enhancements to the library - all driven by feedback from users and the community. Planned for release in January 2021 we also undertook the work to enable isolated runtimes, a long requested feature. This allows you to operate on multiple independently configured \"roots\" such as \"sp\" or \"graph\" from the same application. Previously the library was configured globally, so this opens new possibilities for both client and server side scenarios. Finally we made many tooling and project improvements such as moving to GitHub actions, updating the tests to use MSAL, and exploring ways to enhance the developer experience.","title":"2020 Year End Report"},{"location":"v2/news/2020-year-in-review/#usage","text":"In 2020 we tracked steady month/month growth in raw usage measured by requests as well as in the number of tenants deploying the library. Starting the year we were used in 14605 tenants and by December that number grew to 21,227. These tenants generated 6.1 billion requests to the service in January growing to 9.2 billion by December, peaking at 10.1 billion requests in November. 1) There was a data glitch in October so the numbers do not fully represent usage. 2) These numbers only include public cloud SPO usage, true usage is higher than we can track due to on-premesis and gov/sovereign clouds","title":"Usage"},{"location":"v2/news/2020-year-in-review/#releases","text":"We continued our monthly release cadence as it represents a good pace for addressing issues while not expecting folks to update too often and keeping each update to a reasonable size. All changes can be tracked in our change log , updated with each release. You can check our scheduled releases through project milestones , understanding there are occasionally delays. Monthly releases allows us to ensure bugs do not linger and we continually improve and expand the capabilities of the libraries.","title":"Releases"},{"location":"v2/news/2020-year-in-review/#npm-package-download-statistics-pnpsp","text":"Month Count * Month Count January 100,686 * July 36,805 February 34,437 * August 38,897 March 34,574 * September 45,968 April 32,436 * October 46,655 May 34,482 * November 45,511 June 34,408 * December 58,977 Grand Total 543,836 With 2020 our total all time downloads of @pnp/sp is now at: 949,638 Stats from https://npm-stat.com/","title":"NPM Package download statistics (@pnp/sp):"},{"location":"v2/news/2020-year-in-review/#future-plans","text":"Looking to the future we will continue to actively grow and improve v2 of the library, guided by feedback and reported issues. Additionally, we are beginning to discuss v3 and doing initial planning and prototyping. The v3 work will continue through 2021 with no currently set release date, though we will keep everyone up to date. Additionally in 2021 there will be a general focus on improving not just the code but our tooling, build pipeline, and library contributor experience. We will also look at automatic canary releases with each merge, and other improvements.","title":"Future Plans"},{"location":"v2/news/2020-year-in-review/#new-lead-maintainer","text":"With the close of 2020 we are very excited to announce a new lead maintainer for PnPjs, Julie Turner ! Julie brings deep expertise with SharePoint Framework, TypeScript, and SharePoint development to the team, coupled with dedication and care in the work. Over the last year she has gotten more involved with handling releases, responding to issues, and helping to keep the code updated and clean. We are very lucky to have her working on the project and look forward to seeing her lead the growth and direction for years to come.","title":"New Lead Maintainer"},{"location":"v2/news/2020-year-in-review/#contributors","text":"As always we have abundant thanks and appreciation for your contributors. Taking your time to help improve PnPjs for the community is massive and valuable to ensure our sustainability. Thank you for all your help in 2020! If you are interested in becoming a contributor check out our guide on ways to get started.","title":"Contributors"},{"location":"v2/news/2020-year-in-review/#sponsors","text":"We want to thank our sponsors for their support in 2020! This year we put the money towards helping offset the cost and shipping of hoodies to contributors and sponsors. Your continued generosity makes a big difference in our ability to recognize and reward the folks building PnPjs. Thank You","title":"Sponsors"},{"location":"v2/news/2020-year-in-review/#closing","text":"In closing we want say Thank You to everyone who uses, contributes to, and participates in PnPjs and the SharePoint Patterns and Practices program. Wishing you the very best for 2021, The PnPjs Team","title":"Closing"},{"location":"v2/nodejs/","text":"@pnp/nodejs \u00b6 This package supplies helper code when using the @pnp libraries within the context of nodejs. Primarily these consist of clients to enable use of the libraries in nodejs. Getting Started \u00b6 Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the exported functionality. npm install @pnp/sp @pnp/nodejs --save AdalFetchClient SPFetchClient BearerTokenFetchClient Proxy SP Extensions \u00b6 Added in 2.0.9 A set of nodejs specific extensions for the @pnp/sp library. SP Extensions","title":"@pnp/nodejs"},{"location":"v2/nodejs/#pnpnodejs","text":"This package supplies helper code when using the @pnp libraries within the context of nodejs. Primarily these consist of clients to enable use of the libraries in nodejs.","title":"@pnp/nodejs"},{"location":"v2/nodejs/#getting-started","text":"Install the library and required dependencies. You will also need to install other libraries such as @pnp/sp or @pnp/graph to use the exported functionality. npm install @pnp/sp @pnp/nodejs --save AdalFetchClient SPFetchClient BearerTokenFetchClient Proxy","title":"Getting Started"},{"location":"v2/nodejs/#sp-extensions","text":"Added in 2.0.9 A set of nodejs specific extensions for the @pnp/sp library. SP Extensions","title":"SP Extensions"},{"location":"v2/nodejs/adal-fetch-client/","text":"@pnp/nodejs/adalfetchclient \u00b6 The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"@pnp/nodejs/adalfetchclient"},{"location":"v2/nodejs/adal-fetch-client/#pnpnodejsadalfetchclient","text":"The AdalFetchClient class depends on the adal-node package to authenticate against Azure AD. The example below outlines usage with the @pnp/graph library, though it would work in any case where an Azure AD Bearer token is expected. import { AdalFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new AdalFetchClient(\"{tenant}\", \"{app id}\", \"{app secret}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"@pnp/nodejs/adalfetchclient"},{"location":"v2/nodejs/bearer-token-fetch-client/","text":"@pnp/nodejs/BearerTokenFetchClient \u00b6 The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. import { BearerTokenFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new BearerTokenFetchClient(\"{Bearer Token}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"@pnp/nodejs/BearerTokenFetchClient"},{"location":"v2/nodejs/bearer-token-fetch-client/#pnpnodejsbearertokenfetchclient","text":"The BearerTokenFetchClient class allows you to easily specify your own Bearer tokens to be used in the requests. How you derive the token is up to you. import { BearerTokenFetchClient } from \"@pnp/nodejs\"; import { graph } from \"@pnp/graph/presets/all\"; // setup the client using graph setup function graph.setup({ graph: { fetchClientFactory: () => { return new BearerTokenFetchClient(\"{Bearer Token}\"); }, }, }); // execute a library request as normal const g = await graph.groups(); console.log(JSON.stringify(g, null, 4));","title":"@pnp/nodejs/BearerTokenFetchClient"},{"location":"v2/nodejs/provider-hosted-app/","text":"@pnp/nodejs/providerhostedrequestcontext \u00b6 The ProviderHostedRequestContext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. Note : To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context. import { sp, SPRest } from \"@pnp/sp/presets/all\"; import { NodeFetchClient, ProviderHostedRequestContext } from \"@pnp/nodejs\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new NodeFetchClient(); }, }, }); // get request data generated by /_layouts/15/AppRedirect.aspx const spAppToken = request.body.SPAppToken; const spSiteUrl = request.body.SPSiteUrl; // create a context based on the add-in details and SPAppToken const ctx = await ProviderHostedRequestContext.create(spSiteUrl, \"{client id}\", \"{client secret}\", spAppToken); // create an SPRest object configured to use our context // this is used in place of the global sp object const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl); const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl); // make a request on behalf of the user const user = await userSP.web.currentUser(); console.log(`Hello ${user.Title}`); // make an add-in only request const app = await addinSP.web.currentUser(); console.log(`Add-in principal: ${app.Title}`);","title":"@pnp/nodejs/providerhostedrequestcontext"},{"location":"v2/nodejs/provider-hosted-app/#pnpnodejsproviderhostedrequestcontext","text":"The ProviderHostedRequestContext enables the creation of provider-hosted add-ins built in node.js to use pnpjs to interact with SharePoint. The context is associated to a SharePoint user, allowing requests to be made by the add-in on the behalf of the user. The usage of this class assumes the provider-hosted add-in is called from SharePoint with a valid SPAppToken. This is typically done by means of accessing /_layouts/15/AppRedirect.aspx with the app's client ID and app's redirect URI. Note : To support concurrent requests by different users and/or add-ins on different tenants, do not use the SPFetchClient class. Instead, use the more generic NodeFetchClient class. The downside is that you have to manually configure each request to use the desired user/app context. import { sp, SPRest } from \"@pnp/sp/presets/all\"; import { NodeFetchClient, ProviderHostedRequestContext } from \"@pnp/nodejs\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new NodeFetchClient(); }, }, }); // get request data generated by /_layouts/15/AppRedirect.aspx const spAppToken = request.body.SPAppToken; const spSiteUrl = request.body.SPSiteUrl; // create a context based on the add-in details and SPAppToken const ctx = await ProviderHostedRequestContext.create(spSiteUrl, \"{client id}\", \"{client secret}\", spAppToken); // create an SPRest object configured to use our context // this is used in place of the global sp object const userSP = new SPRest().configure(await ctx.getUserConfig(), spSiteUrl); const addinSP = new SPRest().configure(await ctx.getAddInOnlyConfig(), spSiteUrl); // make a request on behalf of the user const user = await userSP.web.currentUser(); console.log(`Hello ${user.Title}`); // make an add-in only request const app = await addinSP.web.currentUser(); console.log(`Add-in principal: ${app.Title}`);","title":"@pnp/nodejs/providerhostedrequestcontext"},{"location":"v2/nodejs/proxy/","text":"@pnp/nodejs/proxy \u00b6 In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler. setProxyUrl \u00b6 Basic Usage \u00b6 You need to import the setProxyUrl function from @pnp/nodejs library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library. import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // call the set proxy url function and it will be used for all requests regardless of client setProxyUrl(\"{your proxy url}\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, }); Use with Fiddler \u00b6 To get Fiddler to work you may need to set an environment variable. This should only be done for testing! import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // ignore certificate errors: ONLY FOR TESTING!! process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\"; // this is my fiddler url locally setProxyUrl(\"http://127.0.0.1:8888\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, }); setProxyAgent \u00b6 Added in 2.0.11 You need to import the setProxyAgent function from @pnp/nodejs library and call it with your proxy url. You can supply any valid proxy and it will be used. import { SPFetchClient, SPOAuthEnv, setProxyAgent } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { const myAgent = new MyAgentOfSomeType({}); // call the set proxy agent function and it will be used for all requests regardless of client setProxyAgent(myAgent); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"@pnp/nodejs/proxy"},{"location":"v2/nodejs/proxy/#pnpnodejsproxy","text":"In some cases when deploying on node you may need to use a proxy as governed by corporate policy, or perhaps you want to examine the traffic using a tool such as Fiddler.","title":"@pnp/nodejs/proxy"},{"location":"v2/nodejs/proxy/#setproxyurl","text":"","title":"setProxyUrl"},{"location":"v2/nodejs/proxy/#basic-usage","text":"You need to import the setProxyUrl function from @pnp/nodejs library and call it with your proxy url. Once done an https-proxy-agent will be used with each request. This works across all clients within the @pnp/nodejs library. import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // call the set proxy url function and it will be used for all requests regardless of client setProxyUrl(\"{your proxy url}\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"Basic Usage"},{"location":"v2/nodejs/proxy/#use-with-fiddler","text":"To get Fiddler to work you may need to set an environment variable. This should only be done for testing! import { SPFetchClient, SPOAuthEnv, setProxyUrl } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { // ignore certificate errors: ONLY FOR TESTING!! process.env.NODE_TLS_REJECT_UNAUTHORIZED = \"0\"; // this is my fiddler url locally setProxyUrl(\"http://127.0.0.1:8888\"); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"Use with Fiddler"},{"location":"v2/nodejs/proxy/#setproxyagent","text":"Added in 2.0.11 You need to import the setProxyAgent function from @pnp/nodejs library and call it with your proxy url. You can supply any valid proxy and it will be used. import { SPFetchClient, SPOAuthEnv, setProxyAgent } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { const myAgent = new MyAgentOfSomeType({}); // call the set proxy agent function and it will be used for all requests regardless of client setProxyAgent(myAgent); return new SPFetchClient(settings.testing.sp.url, settings.testing.sp.id, settings.testing.sp.secret, SPOAuthEnv.SPO); }, }, });","title":"setProxyAgent"},{"location":"v2/nodejs/sp-extensions/","text":"@pnp/nodejs - sp extensions \u00b6 By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api. This article describes them. These examples use the *-commonjs version of the libraries as they target node, you can read more about the differences . IFile.getStream \u00b6 Allows you to read a response body as a nodejs PassThrough stream. // by importing the the library the node specific extensions are automatically applied import { SPFetchClient, SPNS } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{url}\", \"{id}\", \"{secret}\"); }, }, }); // get the stream const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream(); // see if we have a known length console.log(streamResult.knownLength); // read the stream // this is a very basic example - you can do tons more with streams in node const txt = await new Promise((resolve) => { let data = \"\"; stream.body.on(\"data\", (chunk) => data += chunk); stream.body.on(\"end\", () => resolve(data)); }); IFiles.addChunked \u00b6 Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const files = sp.web.defaultDocumentLibrary.rootFolder.files; await files.addChunked(name, stream, null, true, 10); IFile.setStreamContentChunked \u00b6 Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName(\"file-name.txt\"); await file.setStreamContentChunked(stream); Explicit import \u00b6 If you don't need to import anything from the library, but would like to include the extensions just import the library as shown. // ES Modules: import \"@pnp/nodejs\"; import \"@pnp/nodejs-commonjs\"; // get the stream const streamResult = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream(); Accessing SP Extension Namespace \u00b6 There are classes and interfaces included in extension modules, which you can access through a namespace, \"SPNS\". import { SPNS } from \"@pnp/nodejs-commonjs\"; const parser = new SPNS.StreamParser();","title":"@pnp/nodejs - sp extensions"},{"location":"v2/nodejs/sp-extensions/#pnpnodejs-sp-extensions","text":"By importing anything from the @pnp/nodejs library you automatically get nodejs specific extension methods added into the sp fluent api. This article describes them. These examples use the *-commonjs version of the libraries as they target node, you can read more about the differences .","title":"@pnp/nodejs - sp extensions"},{"location":"v2/nodejs/sp-extensions/#ifilegetstream","text":"Allows you to read a response body as a nodejs PassThrough stream. // by importing the the library the node specific extensions are automatically applied import { SPFetchClient, SPNS } from \"@pnp/nodejs-commonjs\"; import { sp } from \"@pnp/sp-commonjs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{url}\", \"{id}\", \"{secret}\"); }, }, }); // get the stream const streamResult: SPNS.IResponseBodyStream = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream(); // see if we have a known length console.log(streamResult.knownLength); // read the stream // this is a very basic example - you can do tons more with streams in node const txt = await new Promise((resolve) => { let data = \"\"; stream.body.on(\"data\", (chunk) => data += chunk); stream.body.on(\"end\", () => resolve(data)); });","title":"IFile.getStream"},{"location":"v2/nodejs/sp-extensions/#ifilesaddchunked","text":"Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const files = sp.web.defaultDocumentLibrary.rootFolder.files; await files.addChunked(name, stream, null, true, 10);","title":"IFiles.addChunked"},{"location":"v2/nodejs/sp-extensions/#ifilesetstreamcontentchunked","text":"Added in 2.1.0 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/files/folder\"; import * as fs from \"fs\"; const stream = fs.createReadStream(\"{file path}\"); const file = sp.web.defaultDocumentLibrary.rootFolder.files..getByName(\"file-name.txt\"); await file.setStreamContentChunked(stream);","title":"IFile.setStreamContentChunked"},{"location":"v2/nodejs/sp-extensions/#explicit-import","text":"If you don't need to import anything from the library, but would like to include the extensions just import the library as shown. // ES Modules: import \"@pnp/nodejs\"; import \"@pnp/nodejs-commonjs\"; // get the stream const streamResult = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/file.txt\").getStream();","title":"Explicit import"},{"location":"v2/nodejs/sp-extensions/#accessing-sp-extension-namespace","text":"There are classes and interfaces included in extension modules, which you can access through a namespace, \"SPNS\". import { SPNS } from \"@pnp/nodejs-commonjs\"; const parser = new SPNS.StreamParser();","title":"Accessing SP Extension Namespace"},{"location":"v2/nodejs/sp-fetch-client/","text":"@pnp/nodejs/spfetchclient \u00b6 The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. See: How to register a legacy SharePoint application import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web(); console.log(JSON.stringify(w, null, 4)); Set Authentication Environment \u00b6 For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. SPO : (default) for all *.sharepoint.com urls China: for China hosted cloud Germany: for Germany local cloud USDef: USA Defense cloud USGov: USA Government cloud import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.China); }, }, }); Set Realm \u00b6 In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx and copying the GUID value that appears after the \"@\" - this is the realm id. import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.SPO, \"{realm}\"); }, }, });","title":"@pnp/nodejs/spfetchclient"},{"location":"v2/nodejs/sp-fetch-client/#pnpnodejsspfetchclient","text":"The SPFetchClient is used to authentication to SharePoint as a provider hosted add-in using a client and secret in nodejs. Remember it is not a good practice to expose client ids and secrets on the client and use of this class is intended for nodejs exclusively. See: How to register a legacy SharePoint application import { SPFetchClient } from \"@pnp/nodejs\"; import { sp } from \"@pnp/sp/presets/all\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\"); }, }, }); // execute a library request as normal const w = await sp.web(); console.log(JSON.stringify(w, null, 4));","title":"@pnp/nodejs/spfetchclient"},{"location":"v2/nodejs/sp-fetch-client/#set-authentication-environment","text":"For some areas such as Germany, China, and US Gov clouds you need to specify a different authentication url to the service. This is done by specifying the correct SPOAuthEnv enumeration to the SPFetchClient constructor. The options are listed below. If you are not sure which option to specify the default is likely OK. SPO : (default) for all *.sharepoint.com urls China: for China hosted cloud Germany: for Germany local cloud USDef: USA Defense cloud USGov: USA Government cloud import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.China); }, }, });","title":"Set Authentication Environment"},{"location":"v2/nodejs/sp-fetch-client/#set-realm","text":"In some cases automatically resolving the realm may not work. In this case you can set the realm parameter in the SPFetchClient constructor. You can determine the correct value for the realm by navigating to https://{site name}-admin.sharepoint.com/_layouts/15/TA_AllAppPrincipals.aspx and copying the GUID value that appears after the \"@\" - this is the realm id. import { sp } from \"@pnp/sp/presets/all\"; import { SPFetchClient, SPOAuthEnv } from \"@pnp/nodejs\"; sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{site url}\", \"{client id}\", \"{client secret}\", SPOAuthEnv.SPO, \"{realm}\"); }, }, });","title":"Set Realm"},{"location":"v2/odata/","text":"@pnp/queryable \u00b6 This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure the core code is solid and well tested, with any updates benefitting all inheriting libraries. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable --save Library Topics \u00b6 caching core OData Batching Parsers Pipeline Queryable","title":"@pnp/queryable"},{"location":"v2/odata/#pnpqueryable","text":"This modules contains the abstract core classes used to process odata requests. They can also be used to build your own odata library should you wish to. By sharing the core functionality across libraries we can provide a consistent API as well as ensure the core code is solid and well tested, with any updates benefitting all inheriting libraries.","title":"@pnp/queryable"},{"location":"v2/odata/#getting-started","text":"Install the library and required dependencies npm install @pnp/logging @pnp/core @pnp/queryable --save","title":"Getting Started"},{"location":"v2/odata/#library-topics","text":"caching core OData Batching Parsers Pipeline Queryable","title":"Library Topics"},{"location":"v2/odata/caching/","text":"@pnp/queryable/caching \u00b6 Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph. Basic example \u00b6 You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The code below will get items from a list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() method should always be the last method in the chain before the get() (OR if you are using batching these methods can be transposed, more details below). import { sp } from \"@pnp/sp\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.usingCaching()(); console.log(r); const r2 = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r2); Globally Configure Cache Settings \u00b6 If you would not like to use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. import { sp } from \"@pnp/sp\"; sp.setup({ defaultCachingStore: \"session\", // or \"local\" defaultCachingTimeoutSeconds: 30, globalCacheDisable: false // or true to disable caching in case of debugging/testing }); const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r); Per Call Configuration \u00b6 If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. export interface ICachingOptions { expiration?: Date; storeName?: \"session\" | \"local\"; key: string; } import { sp } from \"@pnp/sp\"; import { dateAdd } from \"@pnp/core\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching({ expiration: dateAdd(new Date(), \"minute\", 20), key: \"My Key\", storeName: \"local\" })(); console.log(r); Using Batching with Caching \u00b6 You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. import { sp } from \"@pnp/sp\"; let batch = sp.createBatch(); sp.web.lists.inBatch(batch).usingCaching()().then(r => { console.log(r) }); sp.web.lists.getByTitle(\"Tasks\").items.usingCaching().inBatch(batch)().then(r => { console.log(r) }); batch.execute().then(() => console.log(\"All done!\")); Implement Custom Caching \u00b6 You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here . Implement caching helper method \u00b6 We create a map to act as our cache storage and a function to wrap the request caching logic const map = new Map(); async function staleWhileRevalidate(key: string, p: Promise): Promise { if (map.has(key)) { // In Cache p.then(u => { // Update Cache once we have a result map.set(key, u); }); // Return from Cache return map.get(key); } // Not In Cache so we need to wait for the value const r = await p; // Set Cache map.set(key, r); // Return from Promise return r; } Usage \u00b6 Don't call usingCaching just apply the helper method // this one will wait for the request to finish const r1 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r2, null, 2)); Wrapper Function \u00b6 You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. interface WebData { Title: string; Description: string; } function getWebData(): Promise { return staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); } // this one will wait for the request to finish const r1 = await getWebData(); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await getWebData(); console.log(JSON.stringify(r2, null, 2));","title":"@pnp/queryable/caching"},{"location":"v2/odata/caching/#pnpqueryablecaching","text":"Often times data doesn't change that quickly, especially in the case of rolling up corporate news or upcoming events. These types of things can be cached for minutes if not hours. To help make caching easy you just need to insert the usingCaching method in your chain. This only applies to get requests. The usingCaching method can be used with the inBatch method as well to cache the results of batched requests. The below examples uses the @pnp/sp library as the example - but this works equally well for any library making use of the @pnp/queryable base classes, such as @pnp/graph.","title":"@pnp/queryable/caching"},{"location":"v2/odata/caching/#basic-example","text":"You can use the method without any additional configuration. We have made some default choices for you and will discuss ways to override them later. The code below will get items from a list, first checking the cache for the value. You can also use it with OData operators such as top and orderBy. The usingCaching() method should always be the last method in the chain before the get() (OR if you are using batching these methods can be transposed, more details below). import { sp } from \"@pnp/sp\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.usingCaching()(); console.log(r); const r2 = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r2);","title":"Basic example"},{"location":"v2/odata/caching/#globally-configure-cache-settings","text":"If you would not like to use the default values, but don't want to clutter your code by setting the caching values on each request you can configure custom options globally. These will be applied to all calls to usingCaching() throughout your application. import { sp } from \"@pnp/sp\"; sp.setup({ defaultCachingStore: \"session\", // or \"local\" defaultCachingTimeoutSeconds: 30, globalCacheDisable: false // or true to disable caching in case of debugging/testing }); const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching()(); console.log(r);","title":"Globally Configure Cache Settings"},{"location":"v2/odata/caching/#per-call-configuration","text":"If you prefer more verbose code or have a need to manage the cache settings on a per request basis you can include individual caching settings for each request. These settings are passed to the usingCaching method call and are defined in the following interface. If you want to use the per-request options you must include the key. export interface ICachingOptions { expiration?: Date; storeName?: \"session\" | \"local\"; key: string; } import { sp } from \"@pnp/sp\"; import { dateAdd } from \"@pnp/core\"; const r = await sp.web.lists.getByTitle(\"Tasks\").items.top(5).orderBy(\"Modified\").usingCaching({ expiration: dateAdd(new Date(), \"minute\", 20), key: \"My Key\", storeName: \"local\" })(); console.log(r);","title":"Per Call Configuration"},{"location":"v2/odata/caching/#using-batching-with-caching","text":"You can use batching and caching together, but remember caching is only applied to get requests. When you use them together the methods can be transposed, the below example is valid. import { sp } from \"@pnp/sp\"; let batch = sp.createBatch(); sp.web.lists.inBatch(batch).usingCaching()().then(r => { console.log(r) }); sp.web.lists.getByTitle(\"Tasks\").items.usingCaching().inBatch(batch)().then(r => { console.log(r) }); batch.execute().then(() => console.log(\"All done!\"));","title":"Using Batching with Caching"},{"location":"v2/odata/caching/#implement-custom-caching","text":"You may desire to use a different caching strategy than the one we implemented within the library. The easiest way to achieve this is to wrap the request in your custom caching functionality using the unresolved promise as needed. Here we show how to implement the Stale While Revalidate pattern as discussed here .","title":"Implement Custom Caching"},{"location":"v2/odata/caching/#implement-caching-helper-method","text":"We create a map to act as our cache storage and a function to wrap the request caching logic const map = new Map(); async function staleWhileRevalidate(key: string, p: Promise): Promise { if (map.has(key)) { // In Cache p.then(u => { // Update Cache once we have a result map.set(key, u); }); // Return from Cache return map.get(key); } // Not In Cache so we need to wait for the value const r = await p; // Set Cache map.set(key, r); // Return from Promise return r; }","title":"Implement caching helper method"},{"location":"v2/odata/caching/#usage","text":"Don't call usingCaching just apply the helper method // this one will wait for the request to finish const r1 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); console.log(JSON.stringify(r2, null, 2));","title":"Usage"},{"location":"v2/odata/caching/#wrapper-function","text":"You can wrap this call into a single function you can reuse within your application each time you need the web data for example. You can update the select and interface to match your needs as well. interface WebData { Title: string; Description: string; } function getWebData(): Promise { return staleWhileRevalidate(\"test1\", sp.web.select(\"Title\", \"Description\")()); } // this one will wait for the request to finish const r1 = await getWebData(); console.log(JSON.stringify(r1, null, 2)); // this one will return the result from cache and then update the cache in the background const r2 = await getWebData(); console.log(JSON.stringify(r2, null, 2));","title":"Wrapper Function"},{"location":"v2/odata/core/","text":"@pnp/queryable/core \u00b6 This module contains shared interfaces and abstract classes used within the @pnp/queryable package and those items that inherit from it. ProcessHttpClientResponseException \u00b6 The exception thrown when a response is returned and cannot be processed. interface ODataParser \u00b6 Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the value to be returned. It has two methods, one is optional: parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor ODataParserBase \u00b6 The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods. Create a custom parser from ODataParserBase \u00b6 You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases. class MyParser extends ODataParserBase { // we need to override the parse method to do our custom stuff public parse(r: Response): Promise { // we wrap everything in a promise return new Promise((resolve, reject) => { // lets use the default error handling which returns true for no error // and will call reject with an error if one exists if (this.handleError(r, reject)) { // now we add our custom parsing here r.text().then(txt => { // here we call a made up function to parse the result // this is where we would do our parsing as required myCustomerUnencode(txt).then(v => { resolve(v); }); }); } }); } }","title":"@pnp/queryable/core"},{"location":"v2/odata/core/#pnpqueryablecore","text":"This module contains shared interfaces and abstract classes used within the @pnp/queryable package and those items that inherit from it.","title":"@pnp/queryable/core"},{"location":"v2/odata/core/#processhttpclientresponseexception","text":"The exception thrown when a response is returned and cannot be processed.","title":"ProcessHttpClientResponseException"},{"location":"v2/odata/core/#interface-odataparser","text":"Base interface used to describe a class that that will parse incoming responses. It takes a single type parameter representing the type of the value to be returned. It has two methods, one is optional: parse(r: Response): Promise - main method use to parse a response and return a Promise resolving to an object of type T hydrate?: (d: any) => T - optional method used when getting an object from the cache if it requires calling a constructor","title":"interface ODataParser"},{"location":"v2/odata/core/#odataparserbase","text":"The base class used by all parsers in the @pnp libraries. It is optional to use when creating your own custom parsers, but does contain several helper methods.","title":"ODataParserBase"},{"location":"v2/odata/core/#create-a-custom-parser-from-odataparserbase","text":"You can always create custom parsers for your projects, however it is likely you will not require this step as the default parsers should work for most cases. class MyParser extends ODataParserBase { // we need to override the parse method to do our custom stuff public parse(r: Response): Promise { // we wrap everything in a promise return new Promise((resolve, reject) => { // lets use the default error handling which returns true for no error // and will call reject with an error if one exists if (this.handleError(r, reject)) { // now we add our custom parsing here r.text().then(txt => { // here we call a made up function to parse the result // this is where we would do our parsing as required myCustomerUnencode(txt).then(v => { resolve(v); }); }); } }); } }","title":"Create a custom parser from ODataParserBase"},{"location":"v2/odata/debug/","text":"Debugging Proxy Objects \u00b6 Because all queryables are now represented as Proxy objects you can't immediately see the properties/method of the object or the data stored about the request. In certain debugging scenarios it can help to get visibility into the object that is wrapped by the proxy. To enable this we provide a set of extensions to help. The debug extensions are added by including the import \"@pnp/queryable/debug\"; statement in your project. It should be removed for production. This module provides several methods to help with debugging Queryable Proxy objects. Unwrap \u00b6 The __unwrap() method returns the concrete Queryable instance wrapped by the Proxy. You can then examine this object in various ways or dump it to the console for debugging. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // unwrap the underlying concrete queryable instance const unwrapped = sp.web.__unwrap(); console.log(JSON.stringify(unwrapped, null, 2)); Note: It is not supported to unwrap objects and then use them. It may work in some cases, but this behavior may change as what is contained with the Proxy is an implementation detail and should not be relied upon. Without the Proxy wrapper we make no guarantees. Data \u00b6 All of the information related to a queryable's request is contained within the \"data\" property. If you need to grab that information you can use the __data property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's data const data = sp.web.__data; console.log(JSON.stringify(data, null, 2)); JSON \u00b6 You can also get a representation of the wrapped instance in JSON format consisting of all its own properties and values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's as JSON const data = sp.web.__json(); console.log(JSON.stringify(data, null, 2)); Deep Trace \u00b6 Deep tracing is the ability to write every property and method access to the log. This produces VERY verbose output but can be helpful in situations where you need to trace how things are called and when within the Proxy. You enable deep tracing using the __enableDeepTrace method and disable using __disableDeepTrace . import { Logger, ConsoleListener } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; Logger.subscribe(new ConsoleListener()); // grab an instance to enable deep trace const web = sp.web; // enable deep trace on the instance web.__enableDeepTrace(); const y = await web.lists(); // disable deep trace web.__disableDeepTrace(); The example above produces the following output: Message: get ::> lists Message: get ::> lists Message: get ::> toUrl Message: get ::> toUrl Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> query Message: get ::> query Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: get ::> __disableDeepTrace Message: get ::> __disableDeepTrace","title":"Debugging Proxy Objects"},{"location":"v2/odata/debug/#debugging-proxy-objects","text":"Because all queryables are now represented as Proxy objects you can't immediately see the properties/method of the object or the data stored about the request. In certain debugging scenarios it can help to get visibility into the object that is wrapped by the proxy. To enable this we provide a set of extensions to help. The debug extensions are added by including the import \"@pnp/queryable/debug\"; statement in your project. It should be removed for production. This module provides several methods to help with debugging Queryable Proxy objects.","title":"Debugging Proxy Objects"},{"location":"v2/odata/debug/#unwrap","text":"The __unwrap() method returns the concrete Queryable instance wrapped by the Proxy. You can then examine this object in various ways or dump it to the console for debugging. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // unwrap the underlying concrete queryable instance const unwrapped = sp.web.__unwrap(); console.log(JSON.stringify(unwrapped, null, 2)); Note: It is not supported to unwrap objects and then use them. It may work in some cases, but this behavior may change as what is contained with the Proxy is an implementation detail and should not be relied upon. Without the Proxy wrapper we make no guarantees.","title":"Unwrap"},{"location":"v2/odata/debug/#data","text":"All of the information related to a queryable's request is contained within the \"data\" property. If you need to grab that information you can use the __data property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's data const data = sp.web.__data; console.log(JSON.stringify(data, null, 2));","title":"Data"},{"location":"v2/odata/debug/#json","text":"You can also get a representation of the wrapped instance in JSON format consisting of all its own properties and values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; // get the underlying queryable's as JSON const data = sp.web.__json(); console.log(JSON.stringify(data, null, 2));","title":"JSON"},{"location":"v2/odata/debug/#deep-trace","text":"Deep tracing is the ability to write every property and method access to the log. This produces VERY verbose output but can be helpful in situations where you need to trace how things are called and when within the Proxy. You enable deep tracing using the __enableDeepTrace method and disable using __disableDeepTrace . import { Logger, ConsoleListener } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/queryable/debug\"; Logger.subscribe(new ConsoleListener()); // grab an instance to enable deep trace const web = sp.web; // enable deep trace on the instance web.__enableDeepTrace(); const y = await web.lists(); // disable deep trace web.__disableDeepTrace(); The example above produces the following output: Message: get ::> lists Message: get ::> lists Message: get ::> toUrl Message: get ::> toUrl Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> query Message: get ::> query Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: get ::> data Message: get ::> data Message: get ::> _data Message: get ::> _data Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122352) Beginning GET request (_api/web/lists) Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232122354) Sending request. Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124099) Completing GET request. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: [5912fe3e-6c2a-4538-84ee-eec28a29cfef] (1580232124102) Returning result from pipeline. Set logging to verbose to see data. Data: {} Message: get ::> __disableDeepTrace Message: get ::> __disableDeepTrace","title":"Deep Trace"},{"location":"v2/odata/extensions/","text":"Extensions \u00b6 introduced in 2.0.0 Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invocable . You can control any behavior of the library with extensions. Extensions do not work in ie11 compatibility mode. This is by design. Types of Extensions \u00b6 There are three types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options. Function Extensions \u00b6 The first type is a simple function with a signature: (op: \"apply\" | \"get\" | \"has\" | \"set\", target: T, ...rest: any[]): void This function is passed the current operation as the first argument, currently one of \"apply\", \"get\", \"has\", or \"set\". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures. Named Extensions \u00b6 Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables. import { extendFactory } from \"@pnp/queryable\"; import { sp, List, Lists, IWeb, ILists, List, IList, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeQueryStrValue\"; // create a plain object with the props and methods we want to add/change const myExtensions = { // override the lists property get lists(this: IWeb): ILists { // we will always order our lists by title and select just the Title for ALL calls (just as an example) return Lists(this).orderBy(\"Title\").select(\"Title\"); }, // override the getByTitle method getByTitle: function (this: ILists, title: string): IList { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, `getByTitle('List2')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; // register all the named Extensions extendFactory(Web, myExtensions); // this will use our extension to ensure the lists are ordered const lists = await sp.web.lists(); console.log(JSON.stringify(lists, null, 2)); // we will get the items from List1 but within the extension it is rewritten as List2 const items = await sp.web.lists.getByTitle(\"List1\").items(); console.log(JSON.stringify(items.length, null, 2)); ProxyHandler Extensions \u00b6 You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work. import { extendFactory } from \"@pnp/queryable\"; import { sp, Lists, IWeb, ILists, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeSingleQuote\"; const myExtensions = { get: (target, p: string | number | symbol, _receiver: any) => { switch (p) { case \"getByTitle\": return (title: string) => { // in our example our list has moved, so we rewrite the request on the fly if (title === \"LookupList\") { return List(target, `getByTitle('OrderByList')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(target, `getByTitle('${escapeQueryStrValue(title)}')`); } }; } }, }; extendFactory(Web, myExtensions); const lists = sp.web.lists; const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2)); Registering Extensions \u00b6 You can register Extensions either globally, on an invocable factory, or on a per-object basis, and you can register a single extension or an array of Extensions. Global Registration \u00b6 Globally registering an extension allows you to inject functionality into every invocable that is instantiated within your application. It is important to remember that processing extensions happens on ALL property access and method invocation operations - so global extensions should be used sparingly. import { extendGlobal } from \"@pnp/queryable\"; // we can add a logging method to very verbosely track what things are called in our application extendGlobal((op: string, _target: any, ...rest: any[]): void => { switch (op) { case \"apply\": Logger.write(`${op} ::> ()`, LogLevel.Info); break; case \"has\": case \"get\": case \"set\": Logger.write(`${op} ::> ${rest[0]}`, LogLevel.Info); break; default: Logger.write(`unknown ${op}`, LogLevel.Info); } }); Factory Registration \u00b6 The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList. import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import { IWeb, Web } from \"@pnp/sp/webs\"; import { ILists, Lists } from \"@pnp/sp/lists\"; import { extendFactory } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // sets up the types correctly when importing across your application declare module \"@pnp/sp/webs/types\" { // we need to extend the interface interface IWeb { orderedLists: ILists; } } // sets up the types correctly when importing across your application declare module \"@pnp/sp/lists/types\" { // we need to extend the interface interface ILists { getOrderedListsQuery: (this: ILists) => ILists; } } extendFactory(Web, { // add an ordered lists property get orderedLists(this: IWeb): ILists { return this.lists.getOrderedListsQuery(); }, }); extendFactory(Lists, { // add an ordered lists property getOrderedListsQuery(this: ILists): ILists { return this.top(10).orderBy(\"Title\").select(\"Title\"); }, }); // regardless of how we access the web and lists collections our extensions remain with all new instance based on const web = Web(\"https://tenant.sharepoint.com/sites/dev/\"); const lists1 = await web.orderedLists(); console.log(JSON.stringify(lists1, null, 2)); const lists2 = await Web(\"https://tenant.sharepoint.com/sites/dev/\").orderedLists(); console.log(JSON.stringify(lists2, null, 2)); const lists3 = await sp.web.orderedLists(); console.log(JSON.stringify(lists3, null, 2)); Instance Registration \u00b6 You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances. Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are. Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance. import { extendObj } from \"@pnp/queryable\"; import { sp, List, ILists } from \"@pnp/sp/presets/all\"; const myExtensions = { getByTitle: function (this: ILists, title: string) { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, \"getByTitle('List2')\"); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; const lists = extendObj(sp.web.lists, myExtensions); const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2)); Enable & Disable Extensions and Clear Global Extensions \u00b6 Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed. import { enableExtensions, disableExtensions, clearGlobalExtensions } from \"@pnp/queryable\"; // disable Extensions disableExtensions(); // enable Extensions enableExtensions(); // clear all the globally registered extensions clearGlobalExtensions(); Order of Operations \u00b6 It is important to understand the order in which extensions are executed and when a value is returned. Instance extensions* are always called first, followed by global Extensions - in both cases they are called in the order they were registered. This allows you to perhaps have some global functionality while maintaining the ability to override it again at the instance level. IF an extension returns a value other than undefined that value is returned and no other extensions are processed. *extensions applied via an extended factory are considered instance extensions","title":"Extensions"},{"location":"v2/odata/extensions/#extensions","text":"introduced in 2.0.0 Extending is the concept of overriding or adding functionality into an object or environment without altering the underlying class instances. This can be useful for debugging, testing, or injecting custom functionality. Extensions work with any invocable . You can control any behavior of the library with extensions. Extensions do not work in ie11 compatibility mode. This is by design.","title":"Extensions"},{"location":"v2/odata/extensions/#types-of-extensions","text":"There are three types of Extensions available as well as three methods for registration. You can register any type of extension with any of the registration options.","title":"Types of Extensions"},{"location":"v2/odata/extensions/#function-extensions","text":"The first type is a simple function with a signature: (op: \"apply\" | \"get\" | \"has\" | \"set\", target: T, ...rest: any[]): void This function is passed the current operation as the first argument, currently one of \"apply\", \"get\", \"has\", or \"set\". The second argument is the target instance upon which the operation is being invoked. The remaining parameters vary by the operation being performed, but will match their respective ProxyHandler method signatures.","title":"Function Extensions"},{"location":"v2/odata/extensions/#named-extensions","text":"Named extensions are designed to add or replace a single property or method, though you can register multiple using the same object. These extensions are defined by using an object which has the property/methods you want to override described. Registering named extensions globally will override that operation to all invokables. import { extendFactory } from \"@pnp/queryable\"; import { sp, List, Lists, IWeb, ILists, List, IList, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeQueryStrValue\"; // create a plain object with the props and methods we want to add/change const myExtensions = { // override the lists property get lists(this: IWeb): ILists { // we will always order our lists by title and select just the Title for ALL calls (just as an example) return Lists(this).orderBy(\"Title\").select(\"Title\"); }, // override the getByTitle method getByTitle: function (this: ILists, title: string): IList { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, `getByTitle('List2')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; // register all the named Extensions extendFactory(Web, myExtensions); // this will use our extension to ensure the lists are ordered const lists = await sp.web.lists(); console.log(JSON.stringify(lists, null, 2)); // we will get the items from List1 but within the extension it is rewritten as List2 const items = await sp.web.lists.getByTitle(\"List1\").items(); console.log(JSON.stringify(items.length, null, 2));","title":"Named Extensions"},{"location":"v2/odata/extensions/#proxyhandler-extensions","text":"You can also register a partial ProxyHandler implementation as an extension. You can implement one or more of the ProxyHandler methods as needed. Here we implement the same override of getByTitle globally. This is the most complicated method of creating an extension and assumes an understanding of how ProxyHandlers work. import { extendFactory } from \"@pnp/queryable\"; import { sp, Lists, IWeb, ILists, Web } from \"@pnp/sp/presets/all\"; import { escapeQueryStrValue } from \"@pnp/sp/utils/escapeSingleQuote\"; const myExtensions = { get: (target, p: string | number | symbol, _receiver: any) => { switch (p) { case \"getByTitle\": return (title: string) => { // in our example our list has moved, so we rewrite the request on the fly if (title === \"LookupList\") { return List(target, `getByTitle('OrderByList')`); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(target, `getByTitle('${escapeQueryStrValue(title)}')`); } }; } }, }; extendFactory(Web, myExtensions); const lists = sp.web.lists; const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2));","title":"ProxyHandler Extensions"},{"location":"v2/odata/extensions/#registering-extensions","text":"You can register Extensions either globally, on an invocable factory, or on a per-object basis, and you can register a single extension or an array of Extensions.","title":"Registering Extensions"},{"location":"v2/odata/extensions/#global-registration","text":"Globally registering an extension allows you to inject functionality into every invocable that is instantiated within your application. It is important to remember that processing extensions happens on ALL property access and method invocation operations - so global extensions should be used sparingly. import { extendGlobal } from \"@pnp/queryable\"; // we can add a logging method to very verbosely track what things are called in our application extendGlobal((op: string, _target: any, ...rest: any[]): void => { switch (op) { case \"apply\": Logger.write(`${op} ::> ()`, LogLevel.Info); break; case \"has\": case \"get\": case \"set\": Logger.write(`${op} ::> ${rest[0]}`, LogLevel.Info); break; default: Logger.write(`unknown ${op}`, LogLevel.Info); } });","title":"Global Registration"},{"location":"v2/odata/extensions/#factory-registration","text":"The pattern you will likely find most useful is the ability to extend an invocable factory. This will apply your extensions to all instances created with that factory, meaning all IWebs or ILists will have the extension methods. The example below shows how to add a property to IWeb as well as a method to IList. import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import { IWeb, Web } from \"@pnp/sp/webs\"; import { ILists, Lists } from \"@pnp/sp/lists\"; import { extendFactory } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // sets up the types correctly when importing across your application declare module \"@pnp/sp/webs/types\" { // we need to extend the interface interface IWeb { orderedLists: ILists; } } // sets up the types correctly when importing across your application declare module \"@pnp/sp/lists/types\" { // we need to extend the interface interface ILists { getOrderedListsQuery: (this: ILists) => ILists; } } extendFactory(Web, { // add an ordered lists property get orderedLists(this: IWeb): ILists { return this.lists.getOrderedListsQuery(); }, }); extendFactory(Lists, { // add an ordered lists property getOrderedListsQuery(this: ILists): ILists { return this.top(10).orderBy(\"Title\").select(\"Title\"); }, }); // regardless of how we access the web and lists collections our extensions remain with all new instance based on const web = Web(\"https://tenant.sharepoint.com/sites/dev/\"); const lists1 = await web.orderedLists(); console.log(JSON.stringify(lists1, null, 2)); const lists2 = await Web(\"https://tenant.sharepoint.com/sites/dev/\").orderedLists(); console.log(JSON.stringify(lists2, null, 2)); const lists3 = await sp.web.orderedLists(); console.log(JSON.stringify(lists3, null, 2));","title":"Factory Registration"},{"location":"v2/odata/extensions/#instance-registration","text":"You can also register Extensions on a single object instance, which is often the preferred approach as it will have less of a performance impact across your whole application. This is useful for debugging, overriding methods/properties, or controlling the behavior of specific object instances. Extensions are not transferred to child objects in a fluent chain, be sure you are extending the instance you think you are. Here we show the same override operation of getByTitle on the lists collection, but safely only overriding the single instance. import { extendObj } from \"@pnp/queryable\"; import { sp, List, ILists } from \"@pnp/sp/presets/all\"; const myExtensions = { getByTitle: function (this: ILists, title: string) { // in our example our list has moved, so we rewrite the request on the fly if (title === \"List1\") { return List(this, \"getByTitle('List2')\"); } else { // you can't at this point call the \"base\" method as you will end up in loop within the proxy // so you need to ensure you patch/include any original functionality you need return List(this, `getByTitle('${escapeQueryStrValue(title)}')`); } }, }; const lists = extendObj(sp.web.lists, myExtensions); const items = await lists.getByTitle(\"LookupList\").items(); console.log(JSON.stringify(items.length, null, 2));","title":"Instance Registration"},{"location":"v2/odata/extensions/#enable-disable-extensions-and-clear-global-extensions","text":"Extensions are automatically enabled when you set an extension through any of the above outlined methods. You can disable and enable extensions on demand if needed. import { enableExtensions, disableExtensions, clearGlobalExtensions } from \"@pnp/queryable\"; // disable Extensions disableExtensions(); // enable Extensions enableExtensions(); // clear all the globally registered extensions clearGlobalExtensions();","title":"Enable & Disable Extensions and Clear Global Extensions"},{"location":"v2/odata/extensions/#order-of-operations","text":"It is important to understand the order in which extensions are executed and when a value is returned. Instance extensions* are always called first, followed by global Extensions - in both cases they are called in the order they were registered. This allows you to perhaps have some global functionality while maintaining the ability to override it again at the instance level. IF an extension returns a value other than undefined that value is returned and no other extensions are processed. *extensions applied via an extended factory are considered instance extensions","title":"Order of Operations"},{"location":"v2/odata/odata-batch/","text":"@pnp/queryable/odatabatch \u00b6 This module contains an abstract class used as a base when inheriting libraries support batching. ODataBatchRequestInfo \u00b6 This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will be responsible for processing that info by implementing the abstract executeImpl method. ODataBatch \u00b6 Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp and @pnp/graph modules.","title":"@pnp/queryable/odatabatch"},{"location":"v2/odata/odata-batch/#pnpqueryableodatabatch","text":"This module contains an abstract class used as a base when inheriting libraries support batching.","title":"@pnp/queryable/odatabatch"},{"location":"v2/odata/odata-batch/#odatabatchrequestinfo","text":"This interface defines what each batch needs to know about each request. It is generic in that any library can provide the information but will be responsible for processing that info by implementing the abstract executeImpl method.","title":"ODataBatchRequestInfo"},{"location":"v2/odata/odata-batch/#odatabatch","text":"Base class for building batching support for a library inheriting from @pnp/queryable. You can see implementations of this abstract class in the @pnp/sp and @pnp/graph modules.","title":"ODataBatch"},{"location":"v2/odata/parsers/","text":"@pnp/queryable/parsers \u00b6 This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need. ODataDefaultParser \u00b6 The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. import { sp } from \"@pnp/sp\"; import { JSONParser } from \"@pnp/queryable\"; try { const parser = new JSONParser(); // this always throws a 404 error await sp.web.getList(\"doesn't exist\").get(parser); } catch (e) { // we can check for the property \"isHttpRequestError\" to see if this is an instance of our class // this gets by all the many limitations of subclassing Error and type detection in JavaScript if (e.hasOwnProperty(\"isHttpRequestError\")) { console.log(\"e is HttpRequestError\"); // now we can access the various properties and make use of the response object. // at this point the body is unread console.log(`status: ${e.status}`); console.log(`statusText: ${e.statusText}`); const json = await e.response.clone().json(); console.log(JSON.stringify(json)); const text = await e.response.clone().text(); console.log(text); const headers = e.response.headers; } console.error(e); } TextParser \u00b6 Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files. BlobParser \u00b6 Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files. JSONParser \u00b6 Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files. BufferParser \u00b6 Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files. LambdaParser \u00b6 Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type. import { LambdaParser } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // here a simple parser duplicating the functionality of the JSONParser const parser = new LambdaParser((r: Response) => r.json()); const webDataJson = await sp.web.get(parser); console.log(webDataJson);","title":"@pnp/queryable/parsers"},{"location":"v2/odata/parsers/#pnpqueryableparsers","text":"This modules contains a set of generic parsers. These can be used or extended as needed, though it is likely in most cases the default parser will be all you need.","title":"@pnp/queryable/parsers"},{"location":"v2/odata/parsers/#odatadefaultparser","text":"The simplest parser used to transform a Response into its JSON representation. The default parser will handle errors in a consistent manner throwing an HttpRequestError instance. This class extends Error and adds the response, status, and statusText properties. The response object is unread. You can use this custom error as shown below to gather more information about what went wrong in the request. import { sp } from \"@pnp/sp\"; import { JSONParser } from \"@pnp/queryable\"; try { const parser = new JSONParser(); // this always throws a 404 error await sp.web.getList(\"doesn't exist\").get(parser); } catch (e) { // we can check for the property \"isHttpRequestError\" to see if this is an instance of our class // this gets by all the many limitations of subclassing Error and type detection in JavaScript if (e.hasOwnProperty(\"isHttpRequestError\")) { console.log(\"e is HttpRequestError\"); // now we can access the various properties and make use of the response object. // at this point the body is unread console.log(`status: ${e.status}`); console.log(`statusText: ${e.statusText}`); const json = await e.response.clone().json(); console.log(JSON.stringify(json)); const text = await e.response.clone().text(); console.log(text); const headers = e.response.headers; } console.error(e); }","title":"ODataDefaultParser"},{"location":"v2/odata/parsers/#textparser","text":"Specialized parser used to parse the response using the .text() method with no other processing. Used primarily for files.","title":"TextParser"},{"location":"v2/odata/parsers/#blobparser","text":"Specialized parser used to parse the response using the .blob() method with no other processing. Used primarily for files.","title":"BlobParser"},{"location":"v2/odata/parsers/#jsonparser","text":"Specialized parser used to parse the response using the .json() method with no other processing. Used primarily for files.","title":"JSONParser"},{"location":"v2/odata/parsers/#bufferparser","text":"Specialized parser used to parse the response using the .arrayBuffer() [node] for .buffer() [browser] method with no other processing. Used primarily for files.","title":"BufferParser"},{"location":"v2/odata/parsers/#lambdaparser","text":"Allows you to pass in any handler function you want, called if the request does not result in an error that transforms the raw, unread request into the result type. import { LambdaParser } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; // here a simple parser duplicating the functionality of the JSONParser const parser = new LambdaParser((r: Response) => r.json()); const webDataJson = await sp.web.get(parser); console.log(webDataJson);","title":"LambdaParser"},{"location":"v2/odata/pipeline/","text":"@pnp/queryable/pipeline \u00b6 All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline. interface RequestContext \u00b6 The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required. interface RequestContext { batch: ODataBatch; batchDependency: () => void; cachingOptions: ICachingOptions; hasResult?: boolean; isBatched: boolean; isCached: boolean; options: FetchOptions; parser: ODataParser; pipeline: Array<(c: RequestContext) => Promise>>; requestAbsoluteUrl: string; requestId: string; result?: T; verb: string; clientFactory: () => RequestClient; } requestPipelineMethod decorator \u00b6 The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existence of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. @requestPipelineMethod(true) public static myPipelineMethod(context: RequestContext): Promise> { return new Promise>(resolve => { // do something resolve(context); }); } Default Pipeline \u00b6 logs the start of the request checks the cache for a value based on the context's cache settings sends the request if no value from found in the cache logs the end of the request","title":"@pnp/queryable/pipeline"},{"location":"v2/odata/pipeline/#pnpqueryablepipeline","text":"All of the odata requests processed by @pnp/queryable pass through an extensible request pipeline. Each request is executed in a specific request context defined by the RequestContext interface with the type parameter representing the type ultimately returned at the end a successful processing through the pipeline. Unless you are writing a pipeline method it is unlikely you will ever interact directly with the request pipeline.","title":"@pnp/queryable/pipeline"},{"location":"v2/odata/pipeline/#interface-requestcontextt","text":"The interface that defines the context within which all requests are executed. Note that the pipeline methods to be executed are part of the context. This allows full control over the methods called during a request, and allows for the insertion of any custom methods required. interface RequestContext { batch: ODataBatch; batchDependency: () => void; cachingOptions: ICachingOptions; hasResult?: boolean; isBatched: boolean; isCached: boolean; options: FetchOptions; parser: ODataParser; pipeline: Array<(c: RequestContext) => Promise>>; requestAbsoluteUrl: string; requestId: string; result?: T; verb: string; clientFactory: () => RequestClient; }","title":"interface RequestContext<T>"},{"location":"v2/odata/pipeline/#requestpipelinemethod-decorator","text":"The requestPipelineMethod decorator is used to tag a pipeline method and add functionality to bypass processing if a result is already present in the pipeline. If you would like your method to always run regardless of the existence of a result you can pass true to ensure it will always run. Each pipeline method takes a single argument of the current RequestContext and returns a promise resolving to the RequestContext updated as needed. @requestPipelineMethod(true) public static myPipelineMethod(context: RequestContext): Promise> { return new Promise>(resolve => { // do something resolve(context); }); }","title":"requestPipelineMethod decorator"},{"location":"v2/odata/pipeline/#default-pipeline","text":"logs the start of the request checks the cache for a value based on the context's cache settings sends the request if no value from found in the cache logs the end of the request","title":"Default Pipeline"},{"location":"v2/odata/queryable/","text":"@pnp/queryable/queryable \u00b6 The Queryable class is the base class for all of the libraries building fluent request apis. abstract class ODataQueryable \u00b6 This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls. Properties \u00b6 query \u00b6 Provides access to the query string builder for this url Public Methods \u00b6 concat \u00b6 Directly concatenates the supplied string to the current url, not normalizing \"/\" chars configure \u00b6 Sets custom options for current object and all derived objects accessible via chaining import { ConfigOptions } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; const headers: ConfigOptions = { Accept: 'application/json;odata=nometadata' }; // here we use configure to set the headers value for all child requests of the list instance const list = sp.web.lists.getByTitle(\"List1\").configure({ headers }); // this will use the values set in configure list.items().then(items => console.log(JSON.stringify(items, null, 2)); For reference the ConfigOptions interface is shown below: export interface ConfigOptions { headers?: string[][] | { [key: string]: string } | Headers; mode?: \"navigate\" | \"same-origin\" | \"no-cors\" | \"cors\"; credentials?: \"omit\" | \"same-origin\" | \"include\"; cache?: \"default\" | \"no-store\" | \"reload\" | \"no-cache\" | \"force-cache\" | \"only-if-cached\"; } configureFrom \u00b6 Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance. usingCaching \u00b6 Enables caching for this request. See caching for more details. import { sp } from \"@pnp/sp\" sp.web.usingCaching()().then(...); inBatch \u00b6 Adds this query to the supplied batch toUrl \u00b6 Gets the current url abstract toUrlAndQuery() \u00b6 When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request get \u00b6 Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.","title":"@pnp/queryable/queryable"},{"location":"v2/odata/queryable/#pnpqueryablequeryable","text":"The Queryable class is the base class for all of the libraries building fluent request apis.","title":"@pnp/queryable/queryable"},{"location":"v2/odata/queryable/#abstract-class-odataqueryablebatchtype-extends-odatabatch","text":"This class takes a single type parameter representing the type of the batch implementation object. If your api will not support batching you can create a dummy class here and simply not use the batching calls.","title":"abstract class ODataQueryable<BatchType extends ODataBatch>"},{"location":"v2/odata/queryable/#properties","text":"","title":"Properties"},{"location":"v2/odata/queryable/#query","text":"Provides access to the query string builder for this url","title":"query"},{"location":"v2/odata/queryable/#public-methods","text":"","title":"Public Methods"},{"location":"v2/odata/queryable/#concat","text":"Directly concatenates the supplied string to the current url, not normalizing \"/\" chars","title":"concat"},{"location":"v2/odata/queryable/#configure","text":"Sets custom options for current object and all derived objects accessible via chaining import { ConfigOptions } from \"@pnp/queryable\"; import { sp } from \"@pnp/sp\"; const headers: ConfigOptions = { Accept: 'application/json;odata=nometadata' }; // here we use configure to set the headers value for all child requests of the list instance const list = sp.web.lists.getByTitle(\"List1\").configure({ headers }); // this will use the values set in configure list.items().then(items => console.log(JSON.stringify(items, null, 2)); For reference the ConfigOptions interface is shown below: export interface ConfigOptions { headers?: string[][] | { [key: string]: string } | Headers; mode?: \"navigate\" | \"same-origin\" | \"no-cors\" | \"cors\"; credentials?: \"omit\" | \"same-origin\" | \"include\"; cache?: \"default\" | \"no-store\" | \"reload\" | \"no-cache\" | \"force-cache\" | \"only-if-cached\"; }","title":"configure"},{"location":"v2/odata/queryable/#configurefrom","text":"Sets custom options from another queryable instance's options. Identical to configure except the options are derived from the supplied instance.","title":"configureFrom"},{"location":"v2/odata/queryable/#usingcaching","text":"Enables caching for this request. See caching for more details. import { sp } from \"@pnp/sp\" sp.web.usingCaching()().then(...);","title":"usingCaching"},{"location":"v2/odata/queryable/#inbatch","text":"Adds this query to the supplied batch","title":"inBatch"},{"location":"v2/odata/queryable/#tourl","text":"Gets the current url","title":"toUrl"},{"location":"v2/odata/queryable/#abstract-tourlandquery","text":"When implemented by an inheriting class will build the full url with appropriate query string used to make the actual request","title":"abstract toUrlAndQuery()"},{"location":"v2/odata/queryable/#get","text":"Execute the current request. Takes an optional type parameter allowing for the typing of the value or the user of parsers that will create specific object instances.","title":"get"},{"location":"v2/pnpjs/","text":"PnPjs \u00b6 This package is a rollup package of all the other libraries for scenarios where you would prefer to access all of the code from a single file. Examples would be importing a single file into a script editor webpart or using the library in other ways that benefit from a single file. You will not be able to take advantage of selective imports using this bundle. Our recommendation is to import the packages directly into your project, or to create a custom bundle . This package is mostly provided to help folks with backward-compatibility needs. Script Editor Webpart \u00b6 The below is an example of using the pnp.js bundle within a Script Editor webpart. This script editor example is provided for folks on older version of SharePoint - when possible your first choice is SharePoint Framework. You will need to grab the pnp.js bundle file from the dist folder of the pnpjs package and upload it to a location where you can reference it from without your script editor webparts. *This is included as a reference for backward compatibility. The script editor webpart is no longer available in SharePoint online. In addition, see our General Statement on Polyfills and IE11
    Access Library Features \u00b6 Within the bundle all of the classes and methods are exported at the root object, with the exports from sp and graph libraries contained with NS variables to avoid naming conflicts. So if you need to access say the \"Web\" factory you can do so: const web = pnp.SPNS.Web(\"https://something.sharepoint.com\"); const lists = await web.lists(); pnp.GraphNS.* Individual libraries can also be accessed for their exports: pnp.Logger.subscribe(new pnp.ConsoleListener()); pnp.log.write(\"hello\");","title":"PnPjs"},{"location":"v2/pnpjs/#pnpjs","text":"This package is a rollup package of all the other libraries for scenarios where you would prefer to access all of the code from a single file. Examples would be importing a single file into a script editor webpart or using the library in other ways that benefit from a single file. You will not be able to take advantage of selective imports using this bundle. Our recommendation is to import the packages directly into your project, or to create a custom bundle . This package is mostly provided to help folks with backward-compatibility needs.","title":"PnPjs"},{"location":"v2/pnpjs/#script-editor-webpart","text":"The below is an example of using the pnp.js bundle within a Script Editor webpart. This script editor example is provided for folks on older version of SharePoint - when possible your first choice is SharePoint Framework. You will need to grab the pnp.js bundle file from the dist folder of the pnpjs package and upload it to a location where you can reference it from without your script editor webparts. *This is included as a reference for backward compatibility. The script editor webpart is no longer available in SharePoint online. In addition, see our General Statement on Polyfills and IE11
    ","title":"Script Editor Webpart"},{"location":"v2/pnpjs/#access-library-features","text":"Within the bundle all of the classes and methods are exported at the root object, with the exports from sp and graph libraries contained with NS variables to avoid naming conflicts. So if you need to access say the \"Web\" factory you can do so: const web = pnp.SPNS.Web(\"https://something.sharepoint.com\"); const lists = await web.lists(); pnp.GraphNS.* Individual libraries can also be accessed for their exports: pnp.Logger.subscribe(new pnp.ConsoleListener()); pnp.log.write(\"hello\");","title":"Access Library Features"},{"location":"v2/sp/","text":"@pnp/sp \u00b6 This package contains the fluent api used to call the SharePoint rest services. Getting Started \u00b6 Install the library and required dependencies npm install @pnp/sp --save Import the library into your application and access the root sp object import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; (function main() { // here we will load the current web's title const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`); )() Getting Started: SharePoint Framework \u00b6 Install the library and required dependencies npm install @pnp/sp --save Import the library into your application, update OnInit, and access the root sp object in render import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; const w = await sp.web.select(\"Title\")(); this.domElement.innerHTML = `Web Title: ${w.Title}`; } Getting Started: Nodejs \u00b6 Install the library and required dependencies npm install @pnp/sp @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { SPFetchClient } from \"@pnp/nodejs\"; // do this once per page load sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{your site url}\", \"{your client id}\", \"{your client secret}\"); }, }, }); // now make any calls you need using the configured client const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`);","title":"@pnp/sp"},{"location":"v2/sp/#pnpsp","text":"This package contains the fluent api used to call the SharePoint rest services.","title":"@pnp/sp"},{"location":"v2/sp/#getting-started","text":"Install the library and required dependencies npm install @pnp/sp --save Import the library into your application and access the root sp object import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; (function main() { // here we will load the current web's title const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`); )()","title":"Getting Started"},{"location":"v2/sp/#getting-started-sharepoint-framework","text":"Install the library and required dependencies npm install @pnp/sp --save Import the library into your application, update OnInit, and access the root sp object in render import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // ... public onInit(): Promise { return super.onInit().then(_ => { // other init code may be present sp.setup({ spfxContext: this.context }); }); } // ... public render(): void { // A simple loading message this.domElement.innerHTML = `Loading...`; const w = await sp.web.select(\"Title\")(); this.domElement.innerHTML = `Web Title: ${w.Title}`; }","title":"Getting Started: SharePoint Framework"},{"location":"v2/sp/#getting-started-nodejs","text":"Install the library and required dependencies npm install @pnp/sp @pnp/nodejs --save Import the library into your application, setup the node client, make a request import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { SPFetchClient } from \"@pnp/nodejs\"; // do this once per page load sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{your site url}\", \"{your client id}\", \"{your client secret}\"); }, }, }); // now make any calls you need using the configured client const w = await sp.web.select(\"Title\")(); console.log(`Web Title: ${w.Title}`);","title":"Getting Started: Nodejs"},{"location":"v2/sp/alias-parameters/","text":"@pnp/sp - Aliased Parameters \u00b6 Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders. To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query. Construct a parameter alias \u00b6 Pattern: !@{label name}::{value} Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\" Example without aliasing \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // still works as expected, no aliasing const query = sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 const r = await query(); console.log(r);; Example with aliasing \u00b6 import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 const r = await query(); console.log(r); Example with aliasing and batching \u00b6 Aliasing is supported with batching as well: import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing and batching const batch = sp.web.createBatch(); const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query.inBatch(batch)().then(r => { console.log(r); }); batch.execute();","title":"@pnp/sp - Aliased Parameters"},{"location":"v2/sp/alias-parameters/#pnpsp-aliased-parameters","text":"Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders. To alias a parameter you include the label name, a separator (\"::\") and the value in the string. You also need to prepend a \"!\" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a \"@\" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use \"@p1\" you should use \"@p2\" for a second parameter alias in the same query.","title":"@pnp/sp - Aliased Parameters"},{"location":"v2/sp/alias-parameters/#construct-a-parameter-alias","text":"Pattern: !@{label name}::{value} Example: \"!@p1::\\sites\\dev\" or \"!@p2::\\text.txt\"","title":"Construct a parameter alias"},{"location":"v2/sp/alias-parameters/#example-without-aliasing","text":"import { sp } from \"@pnp/sp/presets/all\"; // still works as expected, no aliasing const query = sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3 const r = await query(); console.log(r);;","title":"Example without aliasing"},{"location":"v2/sp/alias-parameters/#example-with-aliasing","text":"import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 const r = await query(); console.log(r);","title":"Example with aliasing"},{"location":"v2/sp/alias-parameters/#example-with-aliasing-and-batching","text":"Aliasing is supported with batching as well: import { sp } from \"@pnp/sp/presets/all\"; // same query with aliasing and batching const batch = sp.web.createBatch(); const query = sp.web.getFolderByServerRelativeUrl(\"!@p1::/sites/dev/Shared Documents/\").files.select(\"Title\").top(3); console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3 query.inBatch(batch)().then(r => { console.log(r); }); batch.execute();","title":"Example with aliasing and batching"},{"location":"v2/sp/alm/","text":"@pnp/sp/appcatalog \u00b6 The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions. Understanding the App Catalog Hierarchy \u00b6 Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation . Referencing an App Catalog \u00b6 There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // get the current context web's app catalog const catalog = await sp.web.getAppCatalog()(); // you can also chain off the app catalog const apps = await sp.web.getAppCatalog()(); console.log(apps); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // you can get the tenant app catalog (or any app catalog) by using the getTenantAppCatalogWeb method const appCatWeb = await sp.getTenantAppCatalogWeb()(); const appCatalog = await appCatWeb.getAppCatalog()(); // you can get the tenant app catalog (or any app catalog) by passing in a url // get the tenant app catalog const tenantCatalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/appcatalog\")(); // get a different app catalog const catalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/anothersite\")(); // alternatively you can create a new app catalog instance directly by importing the AppCatalog class import { IAppCatalog, AppCatalog } from '@pnp/sp/appcatalog'; const catalog: IAppCatalog = await AppCatalog(\"https://mytenant.sharepoint.com/sites/apps\")(); // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web import { Web } from '@pnp/sp/webs'; import { AppCatalog } from '@pnp/sp/appcatalog'; const web = Web(\"https://mytenant.sharepoint.com/sites/apps\"); const catalog = await AppCatalog(web)(); The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity. List Available Apps \u00b6 The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. // get available apps await catalog(); // get available apps selecting two fields await catalog.select(\"Title\", \"Deployed\")(); Add an App \u00b6 This action must be performed in the context of the tenant app catalog // this represents the file bytes of the app package file const blob = new Blob(); // there is an optional third argument to control overwriting existing files const r = await catalog.add(\"myapp.app\", blob); // this is at its core a file add operation so you have access to the response data as well // as a File instance representing the created file console.log(JSON.stringify(r.data, null, 4)); // all file operations are available const nameData = await r.file.select(\"Name\")(); Get an App \u00b6 You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions const app = await catalog.getAppById(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\")(); Perform app actions \u00b6 Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block. const myAppId = \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"; // deploy await catalog.getAppById(myAppId).deploy(); // retract await catalog.getAppById(myAppId).retract(); // install await catalog.getAppById(myAppId).install(); // uninstall await catalog.getAppById(myAppId).uninstall(); // upgrade await catalog.getAppById(myAppId).upgrade(); // remove await catalog.getAppById(myAppId).remove(); Synchronize a solution/app to the Microsoft Teams App Catalog \u00b6 By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id . // Using the app id await catalog.syncSolutionToTeams(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"); // Using the SharePoint apps item id await catalog.syncSolutionToTeams(\"123\", true); Notes \u00b6 The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.","title":"@pnp/sp/appcatalog"},{"location":"v2/sp/alm/#pnpspappcatalog","text":"The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.","title":"@pnp/sp/appcatalog"},{"location":"v2/sp/alm/#understanding-the-app-catalog-hierarchy","text":"Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation .","title":"Understanding the App Catalog Hierarchy"},{"location":"v2/sp/alm/#referencing-an-app-catalog","text":"There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // get the current context web's app catalog const catalog = await sp.web.getAppCatalog()(); // you can also chain off the app catalog const apps = await sp.web.getAppCatalog()(); console.log(apps); import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import \"@pnp/sp/webs\"; // you can get the tenant app catalog (or any app catalog) by using the getTenantAppCatalogWeb method const appCatWeb = await sp.getTenantAppCatalogWeb()(); const appCatalog = await appCatWeb.getAppCatalog()(); // you can get the tenant app catalog (or any app catalog) by passing in a url // get the tenant app catalog const tenantCatalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/appcatalog\")(); // get a different app catalog const catalog = await sp.web.getAppCatalog(\"https://mytenant.sharepoint.com/sites/anothersite\")(); // alternatively you can create a new app catalog instance directly by importing the AppCatalog class import { IAppCatalog, AppCatalog } from '@pnp/sp/appcatalog'; const catalog: IAppCatalog = await AppCatalog(\"https://mytenant.sharepoint.com/sites/apps\")(); // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web import { Web } from '@pnp/sp/webs'; import { AppCatalog } from '@pnp/sp/appcatalog'; const web = Web(\"https://mytenant.sharepoint.com/sites/apps\"); const catalog = await AppCatalog(web)(); The following examples make use of a variable \"catalog\" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.","title":"Referencing an App Catalog"},{"location":"v2/sp/alm/#list-available-apps","text":"The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select. // get available apps await catalog(); // get available apps selecting two fields await catalog.select(\"Title\", \"Deployed\")();","title":"List Available Apps"},{"location":"v2/sp/alm/#add-an-app","text":"This action must be performed in the context of the tenant app catalog // this represents the file bytes of the app package file const blob = new Blob(); // there is an optional third argument to control overwriting existing files const r = await catalog.add(\"myapp.app\", blob); // this is at its core a file add operation so you have access to the response data as well // as a File instance representing the created file console.log(JSON.stringify(r.data, null, 4)); // all file operations are available const nameData = await r.file.select(\"Name\")();","title":"Add an App"},{"location":"v2/sp/alm/#get-an-app","text":"You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions const app = await catalog.getAppById(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\")();","title":"Get an App"},{"location":"v2/sp/alm/#perform-app-actions","text":"Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block. const myAppId = \"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"; // deploy await catalog.getAppById(myAppId).deploy(); // retract await catalog.getAppById(myAppId).retract(); // install await catalog.getAppById(myAppId).install(); // uninstall await catalog.getAppById(myAppId).uninstall(); // upgrade await catalog.getAppById(myAppId).upgrade(); // remove await catalog.getAppById(myAppId).remove();","title":"Perform app actions"},{"location":"v2/sp/alm/#synchronize-a-solutionapp-to-the-microsoft-teams-app-catalog","text":"By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id . // Using the app id await catalog.syncSolutionToTeams(\"5137dff1-0b79-4ebc-8af4-ca01f7bd393c\"); // Using the SharePoint apps item id await catalog.syncSolutionToTeams(\"123\", true);","title":"Synchronize a solution/app to the Microsoft Teams App Catalog"},{"location":"v2/sp/alm/#notes","text":"The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.","title":"Notes"},{"location":"v2/sp/attachments/","text":"@pnp/sp/attachments \u00b6 The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/attachments\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\"; Get attachments \u00b6 import { sp } from \"@pnp/sp\"; import { IAttachmentInfo } from \"@pnp/sp/attachments\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); // get all the attachments const info: IAttachmentInfo[] = await item.attachmentFiles(); // get a single file by file name const info2: IAttachmentInfo = await item.attachmentFiles.getByName(\"file.txt\")(); // select specific properties using odata operators and use Pick to type the result const info3: Pick[] = await item.attachmentFiles.select(\"ServerRelativeUrl\")(); Add an Attachment \u00b6 You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.add(\"file2.txt\", \"Here is my content\"); Add Multiple \u00b6 This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists\"; import { IAttachmentFileInfo } from \"@pnp/sp/attachments\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); let fileInfos: IAttachmentFileInfo[] = []; fileInfos.push({ name: \"My file name 1\", content: \"string, blob, or array\" }); fileInfos.push({ name: \"My file name 2\", content: \"string, blob, or array\" }); await list.items.getById(2).attachmentFiles.addMultiple(fileInfos); Delete Multiple \u00b6 import { sp } from \"@pnp/sp\"; import { IList } from \"./@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.deleteMultiple(\"1.txt\", \"2.txt\"); Read Attachment Content \u00b6 You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); const text = await item.attachmentFiles.getByName(\"file.txt\").getText(); // use this in the browser, does not work in nodejs const blob = await item.attachmentFiles.getByName(\"file.mp4\").getBlob(); // use this in nodejs const buffer = await item.attachmentFiles.getByName(\"file.mp4\").getBuffer(); // file must be valid json const json = await item.attachmentFiles.getByName(\"file.json\").getJSON(); Update Attachment Content \u00b6 You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").setContent(\"My new content!!!\"); Delete Attachment \u00b6 import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").delete(); Recycle Attachment \u00b6 Delete the attachment and send it to recycle bin import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").recycle(); Recycle Multiple Attachments \u00b6 Delete multiple attachments and send them to recycle bin import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.recycleMultiple(\"1.txt\",\"2.txt\");","title":"@pnp/sp/attachments"},{"location":"v2/sp/attachments/#pnpspattachments","text":"The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/attachments\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/attachments"},{"location":"v2/sp/attachments/#get-attachments","text":"import { sp } from \"@pnp/sp\"; import { IAttachmentInfo } from \"@pnp/sp/attachments\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); // get all the attachments const info: IAttachmentInfo[] = await item.attachmentFiles(); // get a single file by file name const info2: IAttachmentInfo = await item.attachmentFiles.getByName(\"file.txt\")(); // select specific properties using odata operators and use Pick to type the result const info3: Pick[] = await item.attachmentFiles.select(\"ServerRelativeUrl\")();","title":"Get attachments"},{"location":"v2/sp/attachments/#add-an-attachment","text":"You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.add(\"file2.txt\", \"Here is my content\");","title":"Add an Attachment"},{"location":"v2/sp/attachments/#add-multiple","text":"This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining. import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists\"; import { IAttachmentFileInfo } from \"@pnp/sp/attachments\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); let fileInfos: IAttachmentFileInfo[] = []; fileInfos.push({ name: \"My file name 1\", content: \"string, blob, or array\" }); fileInfos.push({ name: \"My file name 2\", content: \"string, blob, or array\" }); await list.items.getById(2).attachmentFiles.addMultiple(fileInfos);","title":"Add Multiple"},{"location":"v2/sp/attachments/#delete-multiple","text":"import { sp } from \"@pnp/sp\"; import { IList } from \"./@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.deleteMultiple(\"1.txt\", \"2.txt\");","title":"Delete Multiple"},{"location":"v2/sp/attachments/#read-attachment-content","text":"You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); const text = await item.attachmentFiles.getByName(\"file.txt\").getText(); // use this in the browser, does not work in nodejs const blob = await item.attachmentFiles.getByName(\"file.mp4\").getBlob(); // use this in nodejs const buffer = await item.attachmentFiles.getByName(\"file.mp4\").getBuffer(); // file must be valid json const json = await item.attachmentFiles.getByName(\"file.json\").getJSON();","title":"Read Attachment Content"},{"location":"v2/sp/attachments/#update-attachment-content","text":"You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library. import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").setContent(\"My new content!!!\");","title":"Update Attachment Content"},{"location":"v2/sp/attachments/#delete-attachment","text":"import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").delete();","title":"Delete Attachment"},{"location":"v2/sp/attachments/#recycle-attachment","text":"Delete the attachment and send it to recycle bin import { sp } from \"@pnp/sp\"; import { IItem } from \"@pnp/sp/items/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const item: IItem = sp.web.lists.getByTitle(\"MyList\").items.getById(1); await item.attachmentFiles.getByName(\"file2.txt\").recycle();","title":"Recycle Attachment"},{"location":"v2/sp/attachments/#recycle-multiple-attachments","text":"Delete multiple attachments and send them to recycle bin import { sp } from \"@pnp/sp\"; import { IList } from \"@pnp/sp/lists/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/attachments\"; const list: IList = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(2).attachmentFiles.recycleMultiple(\"1.txt\",\"2.txt\");","title":"Recycle Multiple Attachments"},{"location":"v2/sp/clientside-pages/","text":"@pnp/sp/clientside-pages \u00b6 The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/clientside-pages\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/clientside-pages\"; Preset: All import { sp, ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/presets/all\"; Create a new Page \u00b6 You can create a new client-side page in several ways, all are equivalent. Create using IWeb.addClientsidePage \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { PromotedState } from \"@pnp/sp/clientside-pages\"; // Create a page providing a file name const page = await sp.web.addClientsidePage(\"mypage1\"); // ... other operations on the page as outlined below // the page is initially not published, you must publish it so it appears for others users await page.save(); // include title and page layout const page2 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // include title, page layout, and specifying the publishing status (Added in 2.0.4) const page3 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page3.save(); Create using CreateClientsidePage method \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import { CreateClientsidePage, PromotedState } from \"@pnp/sp/clientside-pages\"; const page1 = await CreateClientsidePage(sp.web, \"mypage2\", \"My Page Title\"); // you must publish the new page await page1.save(true); // specify the page layout type parameter const page2 = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4) const page2half = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page2half.save(); // use the web factory to create a page in a specific web const page3 = await CreateClientsidePage(Web(\"https://{absolute web url}\"), \"mypage4\", \"My Page Title\"); // you must publish the new page await page3.save(); Load Pages \u00b6 There are a few ways to load pages, each of which results in an IClientsidePage instance being returned. Load using IWeb.loadClientsidePage \u00b6 This method takes a server relative path to the page to load. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // use from the sp.web fluent chain const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); // use the web factory to target a specific web const page2 = await Web(\"https://{absolute web url}\").loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); Load using ClientsidePageFromFile \u00b6 This method takes an IFile instance and loads an IClientsidePage instance. import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath(\"/sites/dev/sitepages/mypage3.aspx\")); Edit Sections and Columns \u00b6 Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections. // our page instance const page: IClientsidePage; // add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12 const section1 = page.addSection(); section1.addColumn(6); section1.addColumn(6); // create a three column layout in a new section const section2 = page.addSection(); section2.addColumn(4); section2.addColumn(4); section2.addColumn(4); // publish our changes await page.save(); Manipulate Sections and Columns \u00b6 // our page instance const page: IClientsidePage; // drop all the columns in this section // this will also DELETE all controls contained in the columns page.sections[1].columns.length = 0; // create a new column layout page.sections[1].addColumn(4); page.sections[1].addColumn(8); // publish our changes await page.save(); Vertical Section \u00b6 The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier. // our page instance const page: IClientsidePage; // add or get a vertical section (handles case where section already exists) const vertSection = page.addVerticalSection(); // **************************************************************** // if you know or want to test if a vertical section is present: if (page.hasVerticalSection) { // access the vertical section (this method will NOT create the section if it does not exist) page.verticalSection.addControl(new ClientsideText(\"hello\")); } else { const vertSection = page.addVerticalSection(); vertSection.addControl(new ClientsideText(\"hello\")); } Reorder Sections \u00b6 // our page instance const page: IClientsidePage; // swap the order of two sections // this will preserve the controls within the columns page.sections = [page.sections[1], page.sections[0]]; // publish our changes await page.save(); Reorder Columns \u00b6 The sections and columns are arrays, so normal array operations work as expected // our page instance const page: IClientsidePage; // swap the order of two columns // this will preserve the controls within the columns page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]]; // publish our changes await page.save(); Clientside Controls \u00b6 Once you have your sections and columns defined you will want to add/edit controls within those columns. Add Text Content \u00b6 import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; page.addSection().addControl(new ClientsideText(\"@pnp/sp is a great library!\")); await page.save(); Add Controls \u00b6 Adding controls involves loading the available client-side part definitions from the server or creating a text part. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // this will be a ClientsidePageComponent array // this can be cached on the client in production scenarios const partDefs = await sp.web.getClientsideWebParts(); // find the definition we want, here by id const partDef = partDefs.filter(c => c.Id === \"490d7c76-1824-45b2-9de3-676421c997fa\"); // optionally ensure you found the def if (partDef.length < 1) { // we didn't find it so we throw an error throw new Error(\"Could not find the web part\"); } // create a ClientWebPart instance from the definition const part = ClientsideWebpart.fromComponentDef(partDef[0]); // set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video. // the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting // the properties correctly part.setProperties<{ embedCode: string }>({ embedCode: \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\", }); // we add that part to a new section page.addSection().addControl(part); await page.save(); Handle Different Webpart's Settings \u00b6 There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // we create a class to wrap our functionality in a reusable way class ListWebpart extends ClientsideWebpart { constructor(control: ClientsideWebpart) { super((control).json); } // add property getter/setter for what we need, in this case \"listTitle\" within searchablePlainTexts public get DisplayTitle(): string { return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || \"\"; } public set DisplayTitle(value: string) { this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value; } } // now we load our page const page = await sp.web.loadClientsidePage(\"/sites/dev/SitePages/List-Web-Part.aspx\"); // get our part and pass it to the constructor of our wrapper class const part = new ListWebpart(page.sections[0].columns[0].getControl(0)); part.DisplayTitle = \"My New Title!\"; await page.save(); Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties. Page Operations \u00b6 There are other operation you can perform on a page in addition to manipulating the content. pageLayout \u00b6 You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously. // our page instance const page: IClientsidePage; // get the current value const value = page.pageLayout; // set the value page.pageLayout = \"Article\"; await page.save(); bannerImageUrl \u00b6 // our page instance const page: IClientsidePage; // get the current value const value = page.bannerImageUrl; // set the value page.bannerImageUrl = \"/server/relative/path/to/image.png\"; await page.save(); Banner images need to exist within the same site collection as the page where you want to use them. thumbnailUrl \u00b6 Allows you to set the thumbnail used for the page independently of the banner. If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality. // our page instance const page: IClientsidePage; // get the current value const value = page.thumbnailUrl; // set the value page.thumbnailUrl = \"/server/relative/path/to/image.png\"; await page.save(); topicHeader \u00b6 // our page instance const page: IClientsidePage; // get the current value const value = page.topicHeader; // set the value page.topicHeader = \"My cool header!\"; await page.save(); // clear the topic header and hide it page.topicHeader = \"\"; await page.save(); title \u00b6 // our page instance const page: IClientsidePage; // get the current value const value = page.title; // set the value page.title = \"My page title\"; await page.save(); description \u00b6 Descriptions are limited to 255 chars // our page instance const page: IClientsidePage; // get the current value const value = page.description; // set the value page.description = \"A description\"; await page.save(); layoutType \u00b6 Sets the layout type of the page. The valid values are: \"FullWidthImage\", \"NoImage\", \"ColorBlock\", \"CutInShape\" // our page instance const page: IClientsidePage; // get the current value const value = page.layoutType; // set the value page.layoutType = \"ColorBlock\"; await page.save(); headerTextAlignment \u00b6 Sets the header text alignment to one of \"Left\" or \"Center\" // our page instance const page: IClientsidePage; // get the current value const value = page.headerTextAlignment; // set the value page.headerTextAlignment = \"Center\"; await page.save(); showTopicHeader \u00b6 Sets if the topic header is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showTopicHeader; // show the header page.showTopicHeader = true; await page.save(); // hide the header page.showTopicHeader = false; await page.save(); showPublishDate \u00b6 Sets if the publish date is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showPublishDate; // show the date page.showPublishDate = true; await page.save(); // hide the date page.showPublishDate = false; await page.save(); Get / Set author details \u00b6 Added in 2.0.4 // our page instance const page: IClientsidePage; // get the author details (string | null) const value = page.authorByLine; // set the author by user id const user = await web.currentUser.select(\"Id\", \"LoginName\")(); const userId = user.Id; const userLogin = user.LoginName; await page.setAuthorById(userId); await page.save(); await page.setAuthorByLoginName(userLogin); await page.save(); you must still save the page after setting the author to persist your changes as shown in the example. load \u00b6 Loads the page from the server. This will overwrite any local unsaved changes. // our page instance const page: IClientsidePage; await page.load(); save \u00b6 Saves any changes to the page, optionally keeping them in draft state. // our page instance const page: IClientsidePage; // changes are published await page.save(); // changes remain in draft await page.save(false); discardPageCheckout \u00b6 Discards any current checkout of the page by the current user. // our page instance const page: IClientsidePage; await page.discardPageCheckout(); promoteToNews \u00b6 Promotes the page as a news article. // our page instance const page: IClientsidePage; await page.promoteToNews(); enableComments & disableComments \u00b6 Used to control the availability of comments on a page. // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments(); findControlById \u00b6 Finds a control within the page by id. import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\"); // you can also type the control const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\"); findControl \u00b6 Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page. // our page instance const page: IClientsidePage; // find the first control whose order is 9 const control = page.findControl((c) => c.order === 9); // iterate all the controls and output the id to the console page.findControl((c) => { console.log(c.id); return false; }); like & unlike \u00b6 Updates the page's like value for the current user. // our page instance const page: IClientsidePage; // like this page await page.like(); // unlike this page await page.unlike(); getLikedByInformation \u00b6 Gets the likes information for this page. // our page instance const page: IClientsidePage; const info = await page.getLikedByInformation(); copy \u00b6 Creates a copy of the page, including all controls. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instance const page: IClientsidePage; // creates a published copy of the page const pageCopy = await page.copy(sp.web, \"newpagename\", \"New Page Title\"); // creates a draft (unpublished) copy of the page const pageCopy2 = await page.copy(sp.web, \"newpagename\", \"New Page Title\", false); // edits to pageCopy2 ... // publish the page pageCopy2.save(); copyTo \u00b6 Copies the contents of a page to another existing page instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instances, loaded in any of the ways shown above const source: IClientsidePage; const target: IClientsidePage; const target2: IClientsidePage; // creates a published copy of the page await source.copyTo(target); // creates a draft (unpublished) copy of the page await source.copyTo(target2, false); // edits to target2... // publish the page target2.save(); setBannerImage \u00b6 Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent. Banner images need to exist within the same site collection as the page where you want to use them. // our page instance const page: IClientsidePage; page.setBannerImage(\"/server/relative/path/to/image.png\"); // save the changes await page.save(); // set additional props page.setBannerImage(\"/server/relative/path/to/image.png\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save(); This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar. import { SPFetchClient } from \"@pnp/nodejs\"; import { join } from \"path\"; import { readFileSync } from \"fs\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/clientside-pages\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{Site Url}\", \"{Client Id}\", \"{Client Secret}\"); }, }, }); // add the banner image const dirname = join(\"C:/path/to/file\", \"img-file.jpg\"); const file: Uint8Array = new Uint8Array(readFileSync(dirname)); const far = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents\").files.add(\"banner.jpg\", file, true); // add the page const page = await sp.web.addClientsidePage(\"MyPage\", \"Page Title\"); // set the banner image page.setBannerImage(far.data.ServerRelativeUrl); // publish the page await page.save(); setBannerImageFromExternalUrl \u00b6 Added in 2.0.12 Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there. // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\"); // save the changes await page.save(); You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save();","title":"@pnp/sp/clientside-pages"},{"location":"v2/sp/clientside-pages/#pnpspclientside-pages","text":"The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/clientside-pages\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/clientside-pages\"; Preset: All import { sp, ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/clientside-pages"},{"location":"v2/sp/clientside-pages/#create-a-new-page","text":"You can create a new client-side page in several ways, all are equivalent.","title":"Create a new Page"},{"location":"v2/sp/clientside-pages/#create-using-iwebaddclientsidepage","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { PromotedState } from \"@pnp/sp/clientside-pages\"; // Create a page providing a file name const page = await sp.web.addClientsidePage(\"mypage1\"); // ... other operations on the page as outlined below // the page is initially not published, you must publish it so it appears for others users await page.save(); // include title and page layout const page2 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // include title, page layout, and specifying the publishing status (Added in 2.0.4) const page3 = await sp.web.addClientsidePage(\"mypage\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page3.save();","title":"Create using IWeb.addClientsidePage"},{"location":"v2/sp/clientside-pages/#create-using-createclientsidepage-method","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import { CreateClientsidePage, PromotedState } from \"@pnp/sp/clientside-pages\"; const page1 = await CreateClientsidePage(sp.web, \"mypage2\", \"My Page Title\"); // you must publish the new page await page1.save(true); // specify the page layout type parameter const page2 = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\"); // you must publish the new page await page2.save(); // specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4) const page2half = await CreateClientsidePage(sp.web, \"mypage3\", \"My Page Title\", \"Article\", PromotedState.PromoteOnPublish); // you must publish the new page, after which the page will immediately be promoted to a news article await page2half.save(); // use the web factory to create a page in a specific web const page3 = await CreateClientsidePage(Web(\"https://{absolute web url}\"), \"mypage4\", \"My Page Title\"); // you must publish the new page await page3.save();","title":"Create using CreateClientsidePage method"},{"location":"v2/sp/clientside-pages/#load-pages","text":"There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.","title":"Load Pages"},{"location":"v2/sp/clientside-pages/#load-using-iwebloadclientsidepage","text":"This method takes a server relative path to the page to load. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // use from the sp.web fluent chain const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); // use the web factory to target a specific web const page2 = await Web(\"https://{absolute web url}\").loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");","title":"Load using IWeb.loadClientsidePage"},{"location":"v2/sp/clientside-pages/#load-using-clientsidepagefromfile","text":"This method takes an IFile instance and loads an IClientsidePage instance. import { sp } from \"@pnp/sp\"; import { ClientsidePageFromFile } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath(\"/sites/dev/sitepages/mypage3.aspx\"));","title":"Load using ClientsidePageFromFile"},{"location":"v2/sp/clientside-pages/#edit-sections-and-columns","text":"Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections. // our page instance const page: IClientsidePage; // add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12 const section1 = page.addSection(); section1.addColumn(6); section1.addColumn(6); // create a three column layout in a new section const section2 = page.addSection(); section2.addColumn(4); section2.addColumn(4); section2.addColumn(4); // publish our changes await page.save();","title":"Edit Sections and Columns"},{"location":"v2/sp/clientside-pages/#manipulate-sections-and-columns","text":"// our page instance const page: IClientsidePage; // drop all the columns in this section // this will also DELETE all controls contained in the columns page.sections[1].columns.length = 0; // create a new column layout page.sections[1].addColumn(4); page.sections[1].addColumn(8); // publish our changes await page.save();","title":"Manipulate Sections and Columns"},{"location":"v2/sp/clientside-pages/#vertical-section","text":"The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier. // our page instance const page: IClientsidePage; // add or get a vertical section (handles case where section already exists) const vertSection = page.addVerticalSection(); // **************************************************************** // if you know or want to test if a vertical section is present: if (page.hasVerticalSection) { // access the vertical section (this method will NOT create the section if it does not exist) page.verticalSection.addControl(new ClientsideText(\"hello\")); } else { const vertSection = page.addVerticalSection(); vertSection.addControl(new ClientsideText(\"hello\")); }","title":"Vertical Section"},{"location":"v2/sp/clientside-pages/#reorder-sections","text":"// our page instance const page: IClientsidePage; // swap the order of two sections // this will preserve the controls within the columns page.sections = [page.sections[1], page.sections[0]]; // publish our changes await page.save();","title":"Reorder Sections"},{"location":"v2/sp/clientside-pages/#reorder-columns","text":"The sections and columns are arrays, so normal array operations work as expected // our page instance const page: IClientsidePage; // swap the order of two columns // this will preserve the controls within the columns page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]]; // publish our changes await page.save();","title":"Reorder Columns"},{"location":"v2/sp/clientside-pages/#clientside-controls","text":"Once you have your sections and columns defined you will want to add/edit controls within those columns.","title":"Clientside Controls"},{"location":"v2/sp/clientside-pages/#add-text-content","text":"import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; page.addSection().addControl(new ClientsideText(\"@pnp/sp is a great library!\")); await page.save();","title":"Add Text Content"},{"location":"v2/sp/clientside-pages/#add-controls","text":"Adding controls involves loading the available client-side part definitions from the server or creating a text part. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // this will be a ClientsidePageComponent array // this can be cached on the client in production scenarios const partDefs = await sp.web.getClientsideWebParts(); // find the definition we want, here by id const partDef = partDefs.filter(c => c.Id === \"490d7c76-1824-45b2-9de3-676421c997fa\"); // optionally ensure you found the def if (partDef.length < 1) { // we didn't find it so we throw an error throw new Error(\"Could not find the web part\"); } // create a ClientWebPart instance from the definition const part = ClientsideWebpart.fromComponentDef(partDef[0]); // set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video. // the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting // the properties correctly part.setProperties<{ embedCode: string }>({ embedCode: \"https://www.youtube.com/watch?v=IWQFZ7Lx-rg\", }); // we add that part to a new section page.addSection().addControl(part); await page.save();","title":"Add Controls"},{"location":"v2/sp/clientside-pages/#handle-different-webparts-settings","text":"There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { ClientsideWebpart } from \"@pnp/sp/clientside-pages\"; // we create a class to wrap our functionality in a reusable way class ListWebpart extends ClientsideWebpart { constructor(control: ClientsideWebpart) { super((control).json); } // add property getter/setter for what we need, in this case \"listTitle\" within searchablePlainTexts public get DisplayTitle(): string { return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || \"\"; } public set DisplayTitle(value: string) { this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value; } } // now we load our page const page = await sp.web.loadClientsidePage(\"/sites/dev/SitePages/List-Web-Part.aspx\"); // get our part and pass it to the constructor of our wrapper class const part = new ListWebpart(page.sections[0].columns[0].getControl(0)); part.DisplayTitle = \"My New Title!\"; await page.save(); Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.","title":"Handle Different Webpart's Settings"},{"location":"v2/sp/clientside-pages/#page-operations","text":"There are other operation you can perform on a page in addition to manipulating the content.","title":"Page Operations"},{"location":"v2/sp/clientside-pages/#pagelayout","text":"You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously. // our page instance const page: IClientsidePage; // get the current value const value = page.pageLayout; // set the value page.pageLayout = \"Article\"; await page.save();","title":"pageLayout"},{"location":"v2/sp/clientside-pages/#bannerimageurl","text":"// our page instance const page: IClientsidePage; // get the current value const value = page.bannerImageUrl; // set the value page.bannerImageUrl = \"/server/relative/path/to/image.png\"; await page.save(); Banner images need to exist within the same site collection as the page where you want to use them.","title":"bannerImageUrl"},{"location":"v2/sp/clientside-pages/#thumbnailurl","text":"Allows you to set the thumbnail used for the page independently of the banner. If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality. // our page instance const page: IClientsidePage; // get the current value const value = page.thumbnailUrl; // set the value page.thumbnailUrl = \"/server/relative/path/to/image.png\"; await page.save();","title":"thumbnailUrl"},{"location":"v2/sp/clientside-pages/#topicheader","text":"// our page instance const page: IClientsidePage; // get the current value const value = page.topicHeader; // set the value page.topicHeader = \"My cool header!\"; await page.save(); // clear the topic header and hide it page.topicHeader = \"\"; await page.save();","title":"topicHeader"},{"location":"v2/sp/clientside-pages/#title","text":"// our page instance const page: IClientsidePage; // get the current value const value = page.title; // set the value page.title = \"My page title\"; await page.save();","title":"title"},{"location":"v2/sp/clientside-pages/#description","text":"Descriptions are limited to 255 chars // our page instance const page: IClientsidePage; // get the current value const value = page.description; // set the value page.description = \"A description\"; await page.save();","title":"description"},{"location":"v2/sp/clientside-pages/#layouttype","text":"Sets the layout type of the page. The valid values are: \"FullWidthImage\", \"NoImage\", \"ColorBlock\", \"CutInShape\" // our page instance const page: IClientsidePage; // get the current value const value = page.layoutType; // set the value page.layoutType = \"ColorBlock\"; await page.save();","title":"layoutType"},{"location":"v2/sp/clientside-pages/#headertextalignment","text":"Sets the header text alignment to one of \"Left\" or \"Center\" // our page instance const page: IClientsidePage; // get the current value const value = page.headerTextAlignment; // set the value page.headerTextAlignment = \"Center\"; await page.save();","title":"headerTextAlignment"},{"location":"v2/sp/clientside-pages/#showtopicheader","text":"Sets if the topic header is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showTopicHeader; // show the header page.showTopicHeader = true; await page.save(); // hide the header page.showTopicHeader = false; await page.save();","title":"showTopicHeader"},{"location":"v2/sp/clientside-pages/#showpublishdate","text":"Sets if the publish date is displayed on a page. // our page instance const page: IClientsidePage; // get the current value const value = page.showPublishDate; // show the date page.showPublishDate = true; await page.save(); // hide the date page.showPublishDate = false; await page.save();","title":"showPublishDate"},{"location":"v2/sp/clientside-pages/#get-set-author-details","text":"Added in 2.0.4 // our page instance const page: IClientsidePage; // get the author details (string | null) const value = page.authorByLine; // set the author by user id const user = await web.currentUser.select(\"Id\", \"LoginName\")(); const userId = user.Id; const userLogin = user.LoginName; await page.setAuthorById(userId); await page.save(); await page.setAuthorByLoginName(userLogin); await page.save(); you must still save the page after setting the author to persist your changes as shown in the example.","title":"Get / Set author details"},{"location":"v2/sp/clientside-pages/#load","text":"Loads the page from the server. This will overwrite any local unsaved changes. // our page instance const page: IClientsidePage; await page.load();","title":"load"},{"location":"v2/sp/clientside-pages/#save","text":"Saves any changes to the page, optionally keeping them in draft state. // our page instance const page: IClientsidePage; // changes are published await page.save(); // changes remain in draft await page.save(false);","title":"save"},{"location":"v2/sp/clientside-pages/#discardpagecheckout","text":"Discards any current checkout of the page by the current user. // our page instance const page: IClientsidePage; await page.discardPageCheckout();","title":"discardPageCheckout"},{"location":"v2/sp/clientside-pages/#promotetonews","text":"Promotes the page as a news article. // our page instance const page: IClientsidePage; await page.promoteToNews();","title":"promoteToNews"},{"location":"v2/sp/clientside-pages/#enablecomments-disablecomments","text":"Used to control the availability of comments on a page. // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments();","title":"enableComments & disableComments"},{"location":"v2/sp/clientside-pages/#findcontrolbyid","text":"Finds a control within the page by id. import { ClientsideText } from \"@pnp/sp/clientside-pages\"; // our page instance const page: IClientsidePage; const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\"); // you can also type the control const control = page.findControlById(\"06d4cdf6-bce6-4200-8b93-667a1b0a6c9d\");","title":"findControlById"},{"location":"v2/sp/clientside-pages/#findcontrol","text":"Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page. // our page instance const page: IClientsidePage; // find the first control whose order is 9 const control = page.findControl((c) => c.order === 9); // iterate all the controls and output the id to the console page.findControl((c) => { console.log(c.id); return false; });","title":"findControl"},{"location":"v2/sp/clientside-pages/#like-unlike","text":"Updates the page's like value for the current user. // our page instance const page: IClientsidePage; // like this page await page.like(); // unlike this page await page.unlike();","title":"like & unlike"},{"location":"v2/sp/clientside-pages/#getlikedbyinformation","text":"Gets the likes information for this page. // our page instance const page: IClientsidePage; const info = await page.getLikedByInformation();","title":"getLikedByInformation"},{"location":"v2/sp/clientside-pages/#copy","text":"Creates a copy of the page, including all controls. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instance const page: IClientsidePage; // creates a published copy of the page const pageCopy = await page.copy(sp.web, \"newpagename\", \"New Page Title\"); // creates a draft (unpublished) copy of the page const pageCopy2 = await page.copy(sp.web, \"newpagename\", \"New Page Title\", false); // edits to pageCopy2 ... // publish the page pageCopy2.save();","title":"copy"},{"location":"v2/sp/clientside-pages/#copyto","text":"Copies the contents of a page to another existing page instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; // our page instances, loaded in any of the ways shown above const source: IClientsidePage; const target: IClientsidePage; const target2: IClientsidePage; // creates a published copy of the page await source.copyTo(target); // creates a draft (unpublished) copy of the page await source.copyTo(target2, false); // edits to target2... // publish the page target2.save();","title":"copyTo"},{"location":"v2/sp/clientside-pages/#setbannerimage","text":"Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent. Banner images need to exist within the same site collection as the page where you want to use them. // our page instance const page: IClientsidePage; page.setBannerImage(\"/server/relative/path/to/image.png\"); // save the changes await page.save(); // set additional props page.setBannerImage(\"/server/relative/path/to/image.png\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save(); This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar. import { SPFetchClient } from \"@pnp/nodejs\"; import { join } from \"path\"; import { readFileSync } from \"fs\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/clientside-pages\"; // configure your node options sp.setup({ sp: { fetchClientFactory: () => { return new SPFetchClient(\"{Site Url}\", \"{Client Id}\", \"{Client Secret}\"); }, }, }); // add the banner image const dirname = join(\"C:/path/to/file\", \"img-file.jpg\"); const file: Uint8Array = new Uint8Array(readFileSync(dirname)); const far = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents\").files.add(\"banner.jpg\", file, true); // add the page const page = await sp.web.addClientsidePage(\"MyPage\", \"Page Title\"); // set the banner image page.setBannerImage(far.data.ServerRelativeUrl); // publish the page await page.save();","title":"setBannerImage"},{"location":"v2/sp/clientside-pages/#setbannerimagefromexternalurl","text":"Added in 2.0.12 Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there. // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\"); // save the changes await page.save(); You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage // our page instance const page: IClientsidePage; // you must await this method await page.setBannerImageFromExternalUrl(\"https://absolute.url/to/my/image.jpg\", { altText: \"Image description\", imageSourceType: 2, translateX: 30, translateY: 1234, }); // save the changes await page.save();","title":"setBannerImageFromExternalUrl"},{"location":"v2/sp/column-defaults/","text":"@pnp/sp/column-defaults \u00b6 The column defaults sub-module allows you to manage the default column values on a library or library folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/column-defaults\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/column-defaults\"; Preset: All import { sp, IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/presents/all\"; Get Folder Defaults \u00b6 You can get the default values for a specific folder as shown below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" } ] */ Set Folder Defaults \u00b6 When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").setDefaultColumnValues([{ name: \"TextField\", value: \"Something\", }, { name: \"NumberField\", value: 14, }]); Get Library Defaults \u00b6 You can also get all of the defaults for the entire library. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.lists.getByTitle(\"DefaultColumnValues\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{a different default value}\" } ] */ Set Library Defaults \u00b6 You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([{ name: \"TextField\", path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }]); Clear Folder Defaults \u00b6 If you want to clear all of the folder defaults you can use the clear method: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").clearDefaultColumnValues(); Clear Library Defaults \u00b6 If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([]); Pattern for setting defaults on various column types \u00b6 The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types. [{ // Text/Boolean/CurrencyDateTime/Choice/User name: \"TextField\": path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }, { //Number name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: 42, }, { //Date name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"1900-01-01T00:00:00Z\", }, { //Date - Today name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"[today]\", }, { //MultiChoice name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues\", value: [\"Item 1\", \"Item 2\"], }, { //MultiChoice - single value name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues/folder2\", value: [\"Item 1\"], }, { //Taxonomy - single value name: \"TaxonomyField\", path: \"/sites/dev/DefaultColumnValues\", value: { wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" } }, { //Taxonomy - multiple value name: \"TaxonomyMultiField\", path: \"/sites/dev/DefaultColumnValues\", value: [{ wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" },{ wssId:\"-1\", termName: \"TaxValueName2\", termId: \"95d4c307-dde5-49d8-b861-392e145d94d3\" },] }]); Taxonomy Full Example \u00b6 This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/column-defaults\"; import \"@pnp/sp/taxonomy\"; // get the term's info we want to use as the default const term = await sp.termStore.sets.getById(\"ea6fc521-d293-4f3d-9e84-f3a5bc0936ce\").getTermById(\"775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a\")(); // get the default term label const defLabel = term.labels.find(v => v.isDefault); // set the default value using -1, the term id, and the term's default label name await sp.web.lists.getByTitle(\"MetaDataDocLib\").rootFolder.setDefaultColumnValues([{ name: \"MetaDataColumnInternalName\", value: { wssId: \"-1\", termId: term.id, termName: defLabel.name, } }]) // check that the defaults have updated const newDefaults = await sp.web.lists.getByTitle(\"MetaDataDocLib\").getDefaultColumnValues();","title":"@pnp/sp/column-defaults"},{"location":"v2/sp/column-defaults/#pnpspcolumn-defaults","text":"The column defaults sub-module allows you to manage the default column values on a library or library folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/column-defaults\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/column-defaults\"; Preset: All import { sp, IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from \"@pnp/sp/presents/all\";","title":"@pnp/sp/column-defaults"},{"location":"v2/sp/column-defaults/#get-folder-defaults","text":"You can get the default values for a specific folder as shown below: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{the default value}\" } ] */","title":"Get Folder Defaults"},{"location":"v2/sp/column-defaults/#set-folder-defaults","text":"When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").setDefaultColumnValues([{ name: \"TextField\", value: \"Something\", }, { name: \"NumberField\", value: 14, }]);","title":"Set Folder Defaults"},{"location":"v2/sp/column-defaults/#get-library-defaults","text":"You can also get all of the defaults for the entire library. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; const defaults = await sp.web.lists.getByTitle(\"DefaultColumnValues\").getDefaultColumnValues(); /* The resulting structure will have the form: [ { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues\", \"value\": \"{the default value}\" }, { \"name\": \"{field internal name}\", \"path\": \"/sites/dev/DefaultColumnValues/fld_GHk5\", \"value\": \"{a different default value}\" } ] */","title":"Get Library Defaults"},{"location":"v2/sp/column-defaults/#set-library-defaults","text":"You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value. For more examples of other field types see the section Pattern for setting defaults on various column types Note: Be very careful when setting the path as the site collection url is case sensitive import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([{ name: \"TextField\", path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }]);","title":"Set Library Defaults"},{"location":"v2/sp/column-defaults/#clear-folder-defaults","text":"If you want to clear all of the folder defaults you can use the clear method: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.getFolderByServerRelativePath(\"/sites/dev/DefaultColumnValues/fld_GHk5\").clearDefaultColumnValues();","title":"Clear Folder Defaults"},{"location":"v2/sp/column-defaults/#clear-library-defaults","text":"If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/column-defaults\"; await sp.web.lists.getByTitle(\"DefaultColumnValues\").setDefaultColumnValues([]);","title":"Clear Library Defaults"},{"location":"v2/sp/column-defaults/#pattern-for-setting-defaults-on-various-column-types","text":"The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types. [{ // Text/Boolean/CurrencyDateTime/Choice/User name: \"TextField\": path: \"/sites/dev/DefaultColumnValues\", value: \"#PnPjs Rocks!\", }, { //Number name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: 42, }, { //Date name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"1900-01-01T00:00:00Z\", }, { //Date - Today name: \"NumberField\", path: \"/sites/dev/DefaultColumnValues\", value: \"[today]\", }, { //MultiChoice name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues\", value: [\"Item 1\", \"Item 2\"], }, { //MultiChoice - single value name: \"MultiChoiceField\", path: \"/sites/dev/DefaultColumnValues/folder2\", value: [\"Item 1\"], }, { //Taxonomy - single value name: \"TaxonomyField\", path: \"/sites/dev/DefaultColumnValues\", value: { wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" } }, { //Taxonomy - multiple value name: \"TaxonomyMultiField\", path: \"/sites/dev/DefaultColumnValues\", value: [{ wssId:\"-1\", termName: \"TaxValueName\", termId: \"924d2077-d5e3-4507-9f36-4a3655e74274\" },{ wssId:\"-1\", termName: \"TaxValueName2\", termId: \"95d4c307-dde5-49d8-b861-392e145d94d3\" },] }]);","title":"Pattern for setting defaults on various column types"},{"location":"v2/sp/column-defaults/#taxonomy-full-example","text":"This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/column-defaults\"; import \"@pnp/sp/taxonomy\"; // get the term's info we want to use as the default const term = await sp.termStore.sets.getById(\"ea6fc521-d293-4f3d-9e84-f3a5bc0936ce\").getTermById(\"775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a\")(); // get the default term label const defLabel = term.labels.find(v => v.isDefault); // set the default value using -1, the term id, and the term's default label name await sp.web.lists.getByTitle(\"MetaDataDocLib\").rootFolder.setDefaultColumnValues([{ name: \"MetaDataColumnInternalName\", value: { wssId: \"-1\", termId: term.id, termName: defLabel.name, } }]) // check that the defaults have updated const newDefaults = await sp.web.lists.getByTitle(\"MetaDataDocLib\").getDefaultColumnValues();","title":"Taxonomy Full Example"},{"location":"v2/sp/comments-likes/","text":"@pnp/sp/comments and likes \u00b6 Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles. These APIs are currently in BETA and are subject to change or may not work on all tenants. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; ClientsidePage Comments \u00b6 The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately. Add Comments \u00b6 You can add a comment using the addComment method as shown import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\"); Get Page Comments \u00b6 import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); const comments = await page.getComments(); enableComments & disableComments \u00b6 Used to control the availability of comments on a page // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments(); GetById \u00b6 import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\"); const commentData = await page.getCommentById(parseInt(comment.id, 10)); Clear Comments \u00b6 Item Comments \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/comments/item\"; const item = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/SitePages/Test_8q5L.aspx\").getItem(); // as an example, or any of the below options await item.like(); The below examples use a variable named \"item\" which is taken to represent an IItem instance. Comments \u00b6 Get Item Comments \u00b6 const comments = await item.comments(); You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: import { spODataEntityArray } from \"@pnp/sp/odata\"; import { Comment, ICommentData } from \"@pnp/sp/comments\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" }); //load the top 20 replies and comments for an item including likedBy information const comments = await item.comments.expand(\"replies\", \"likedBy\", \"replies/likedBy\").top(20)(); Add Comment \u00b6 // you can add a comment as a string item.comments.add(\"string comment\"); // or you can add it as an object to include mentions item.comments.add({ text: \"comment from object property\" }); Delete a Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].delete() Like Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].like() Unlike Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); comments[0].unlike() Reply to a Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const comment: Comment & CommentData = await comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" }); Load Replies to a Comment \u00b6 import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const replies = await comments[0].replies(); Like \u00b6 You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/item\"; import { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\"; // like an item await item.like(); // unlike an item await item.unlike(); // get the liked by data const likedByData: ILikeData[] = await item.getLikedBy(); // get the liked by information const likedByInfo: ILikedByInformation = await item.getLikedByInformation(); To like/unlike a client-side page and get liked by information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/clientside-page\"; import { ILikedByInformation } from \"@pnp/sp/comments\"; // like a page await page.like(); // unlike a page await page.unlike(); // get the liked by information const likedByInfo: ILikedByInformation = await page.getLikedByInformation();","title":"@pnp/sp/comments and likes"},{"location":"v2/sp/comments-likes/#pnpspcomments-and-likes","text":"Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles. These APIs are currently in BETA and are subject to change or may not work on all tenants. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/comments and likes"},{"location":"v2/sp/comments-likes/#clientsidepage-comments","text":"The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.","title":"ClientsidePage Comments"},{"location":"v2/sp/comments-likes/#add-comments","text":"You can add a comment using the addComment method as shown import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\");","title":"Add Comments"},{"location":"v2/sp/comments-likes/#get-page-comments","text":"import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); await page.addComment(\"A test comment\"); const comments = await page.getComments();","title":"Get Page Comments"},{"location":"v2/sp/comments-likes/#enablecomments-disablecomments","text":"Used to control the availability of comments on a page // you need to import the comments sub-module or use the all preset import \"@pnp/sp/comments/clientside-page\"; // our page instance const page: IClientsidePage; // turn on comments await page.enableComments(); // turn off comments await page.disableComments();","title":"enableComments & disableComments"},{"location":"v2/sp/comments-likes/#getbyid","text":"import { CreateClientsidePage } from \"@pnp/sp/clientside-pages\"; import \"@pnp/sp/comments/clientside-page\"; const page = await CreateClientsidePage(sp.web, \"mypage\", \"My Page Title\", \"Article\"); // optionally publish the page first await page.save(); const comment = await page.addComment(\"A test comment\"); const commentData = await page.getCommentById(parseInt(comment.id, 10));","title":"GetById"},{"location":"v2/sp/comments-likes/#clear-comments","text":"","title":"Clear Comments"},{"location":"v2/sp/comments-likes/#item-comments","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; import \"@pnp/sp/items\"; import \"@pnp/sp/comments/item\"; const item = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/SitePages/Test_8q5L.aspx\").getItem(); // as an example, or any of the below options await item.like(); The below examples use a variable named \"item\" which is taken to represent an IItem instance.","title":"Item Comments"},{"location":"v2/sp/comments-likes/#comments","text":"","title":"Comments"},{"location":"v2/sp/comments-likes/#get-item-comments","text":"const comments = await item.comments(); You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods: import { spODataEntityArray } from \"@pnp/sp/odata\"; import { Comment, ICommentData } from \"@pnp/sp/comments\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" }); //load the top 20 replies and comments for an item including likedBy information const comments = await item.comments.expand(\"replies\", \"likedBy\", \"replies/likedBy\").top(20)();","title":"Get Item Comments"},{"location":"v2/sp/comments-likes/#add-comment","text":"// you can add a comment as a string item.comments.add(\"string comment\"); // or you can add it as an object to include mentions item.comments.add({ text: \"comment from object property\" });","title":"Add Comment"},{"location":"v2/sp/comments-likes/#delete-a-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].delete()","title":"Delete a Comment"},{"location":"v2/sp/comments-likes/#like-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); // these will be Comment instances in the array comments[0].like()","title":"Like Comment"},{"location":"v2/sp/comments-likes/#unlike-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); comments[0].unlike()","title":"Unlike Comment"},{"location":"v2/sp/comments-likes/#reply-to-a-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const comment: Comment & CommentData = await comments[0].replies.add({ text: \"#PnPjs is pretty ok!\" });","title":"Reply to a Comment"},{"location":"v2/sp/comments-likes/#load-replies-to-a-comment","text":"import { spODataEntityArray, Comment, CommentData } from \"@pnp/sp\"; const comments = await item.comments(spODataEntityArray(Comment)); const replies = await comments[0].replies();","title":"Load Replies to a Comment"},{"location":"v2/sp/comments-likes/#like","text":"You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/item\"; import { ILikeData, ILikedByInformation } from \"@pnp/sp/comments\"; // like an item await item.like(); // unlike an item await item.unlike(); // get the liked by data const likedByData: ILikeData[] = await item.getLikedBy(); // get the liked by information const likedByInfo: ILikedByInformation = await item.getLikedByInformation(); To like/unlike a client-side page and get liked by information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/comments/clientside-page\"; import { ILikedByInformation } from \"@pnp/sp/comments\"; // like a page await page.like(); // unlike a page await page.unlike(); // get the liked by information const likedByInfo: ILikedByInformation = await page.getLikedByInformation();","title":"Like"},{"location":"v2/sp/content-types/","text":"@pnp/sp/content-types \u00b6 Content Types are used to define sets of columns in SharePoint. IContentTypes \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { ContentTypes, IContentTypes } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentTypes, IContentTypes } from \"@pnp/sp/presets/all\"; Add an existing Content Type to a collection \u00b6 The following example shows how to add the built in Picture Content Type to the Documents library. sp.web.lists.getByTitle(\"Documents\").contentTypes.addAvailableContentType(\"0x010102\"); Get a Content Type by Id \u00b6 const d: IContentType = await sp.web.contentTypes.getById(\"0x01\")(); // log content type name to console console.log(d.name); Add a new Content Type \u00b6 To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\"); It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). //Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings) sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\", \"This is my content type.\", \"_PnP Content Types\", { ReadOnly: true }); IContentType \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ContentType, IContentType } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentType, IContentType } from \"@pnp/sp/presets/all\"; Get the field links \u00b6 Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type. // get field links from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fieldLinks(); // log collection of fieldlinks to console console.log(d); Get Content Type fields \u00b6 To get a collection with all fields on the Content Type, simply use this method. // get fields from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fields(); // log collection of fields to console console.log(d); Get parent Content Type \u00b6 // get parent Content Type from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").parent(); // log name of parent Content Type to console console.log(d.Name) Get Content Type Workflow associations \u00b6 // get workflow associations from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").workflowAssociations(); // log collection of workflow associations to console console.log(d);","title":"@pnp/sp/content-types"},{"location":"v2/sp/content-types/#pnpspcontent-types","text":"Content Types are used to define sets of columns in SharePoint.","title":"@pnp/sp/content-types"},{"location":"v2/sp/content-types/#icontenttypes","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { ContentTypes, IContentTypes } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentTypes, IContentTypes } from \"@pnp/sp/presets/all\";","title":"IContentTypes"},{"location":"v2/sp/content-types/#add-an-existing-content-type-to-a-collection","text":"The following example shows how to add the built in Picture Content Type to the Documents library. sp.web.lists.getByTitle(\"Documents\").contentTypes.addAvailableContentType(\"0x010102\");","title":"Add an existing Content Type to a collection"},{"location":"v2/sp/content-types/#get-a-content-type-by-id","text":"const d: IContentType = await sp.web.contentTypes.getById(\"0x01\")(); // log content type name to console console.log(d.name);","title":"Get a Content Type by Id"},{"location":"v2/sp/content-types/#add-a-new-content-type","text":"To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\"); It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). //Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings) sp.web.contentTypes.add(\"0x01008D19F38845B0884EBEBE239FDF359184\", \"My Content Type\", \"This is my content type.\", \"_PnP Content Types\", { ReadOnly: true });","title":"Add a new Content Type"},{"location":"v2/sp/content-types/#icontenttype","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { ContentType, IContentType } from \"@pnp/sp/content-types\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/content-types\"; Preset: All import { sp, ContentType, IContentType } from \"@pnp/sp/presets/all\";","title":"IContentType"},{"location":"v2/sp/content-types/#get-the-field-links","text":"Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type. // get field links from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fieldLinks(); // log collection of fieldlinks to console console.log(d);","title":"Get the field links"},{"location":"v2/sp/content-types/#get-content-type-fields","text":"To get a collection with all fields on the Content Type, simply use this method. // get fields from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").fields(); // log collection of fields to console console.log(d);","title":"Get Content Type fields"},{"location":"v2/sp/content-types/#get-parent-content-type","text":"// get parent Content Type from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").parent(); // log name of parent Content Type to console console.log(d.Name)","title":"Get parent Content Type"},{"location":"v2/sp/content-types/#get-content-type-workflow-associations","text":"// get workflow associations from built in Content Type Document (Id: \"0x0101\") const d = await sp.web.contentTypes.getById(\"0x0101\").workflowAssociations(); // log collection of workflow associations to console console.log(d);","title":"Get Content Type Workflow associations"},{"location":"v2/sp/custom-irequestclient/","text":"Custom IRequestClient \u00b6 Scenario: You have some special requirements involving auth scenarios or other needs that the library can't directly support. You may need to create a custom IRequestClient implementation to meet those needs as we can't customize the library to handle every case. This article walks you through how to create a custom IRequestClient and register it for use by the library. It is very unlikely this is a step you ever need to take and we encourage you to ask a question in the issues list before going down this path. Create the Client \u00b6 The easiest way to create a new IRequestClient is to subclass the existing SPHttpClient. You can always write a full client from scratch so long as it supports the IRequestClient interface but you need to handle all of the logic for retry, headers, and the request digest. Here we show implementing a client to solve the need discussed in pull request 1264 as an example. // we subclass SPHttpClient class CustomSPHttpClient extends SPHttpClient { // optionally add a constructor, done here as an example constructor(impl?: IHttpClientImpl) { super(impl); } // override the fetchRaw method to ensure we always include the credentials = \"include\" option // you could also override fetch, but fetchRaw ensures no matter what all requests get your custom logic is applied public fetchRaw(url: string, options?: IFetchOptions): Promise { options.credentials = \"include\"; return super.fetchRaw(url, options); } } The final step is to register the custom client with the library so it is used instead of the default. For that we import the registerCustomRequestClientFactory function and call it before our request generating code. You can reset to the default client factory by passing null to this same function. import { sp, registerCustomRequestClientFactory } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; registerCustomRequestClientFactory(() => new CustomSPHttpClient()); // configure your other options sp.setup({ // ... }); // this request will be executed through your custom client const w = await sp.web(); Unregister Custom Client \u00b6 // unregister custom client factory registerCustomRequestClientFactory(null); IRequestClient Interface \u00b6 If you want to 100% roll your own client you need to implement the below interface, found in common. import { IRequestClient } from \"@pnp/core\"; export interface IRequestClient { fetch(url: string, options?: IFetchOptions): Promise; fetchRaw(url: string, options?: IFetchOptions): Promise; get(url: string, options?: IFetchOptions): Promise; post(url: string, options?: IFetchOptions): Promise; patch(url: string, options?: IFetchOptions): Promise; delete(url: string, options?: IFetchOptions): Promise; } Supportability Note \u00b6 We cannot provide support for your custom client implementation, and creating your own client assumes an intimate knowledge of how SharePoint requests work. Again, this is very likely something you will never need to do - and we recommend exhausting all other options before taking this route.","title":"Custom IRequestClient"},{"location":"v2/sp/custom-irequestclient/#custom-irequestclient","text":"Scenario: You have some special requirements involving auth scenarios or other needs that the library can't directly support. You may need to create a custom IRequestClient implementation to meet those needs as we can't customize the library to handle every case. This article walks you through how to create a custom IRequestClient and register it for use by the library. It is very unlikely this is a step you ever need to take and we encourage you to ask a question in the issues list before going down this path.","title":"Custom IRequestClient"},{"location":"v2/sp/custom-irequestclient/#create-the-client","text":"The easiest way to create a new IRequestClient is to subclass the existing SPHttpClient. You can always write a full client from scratch so long as it supports the IRequestClient interface but you need to handle all of the logic for retry, headers, and the request digest. Here we show implementing a client to solve the need discussed in pull request 1264 as an example. // we subclass SPHttpClient class CustomSPHttpClient extends SPHttpClient { // optionally add a constructor, done here as an example constructor(impl?: IHttpClientImpl) { super(impl); } // override the fetchRaw method to ensure we always include the credentials = \"include\" option // you could also override fetch, but fetchRaw ensures no matter what all requests get your custom logic is applied public fetchRaw(url: string, options?: IFetchOptions): Promise { options.credentials = \"include\"; return super.fetchRaw(url, options); } } The final step is to register the custom client with the library so it is used instead of the default. For that we import the registerCustomRequestClientFactory function and call it before our request generating code. You can reset to the default client factory by passing null to this same function. import { sp, registerCustomRequestClientFactory } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; registerCustomRequestClientFactory(() => new CustomSPHttpClient()); // configure your other options sp.setup({ // ... }); // this request will be executed through your custom client const w = await sp.web();","title":"Create the Client"},{"location":"v2/sp/custom-irequestclient/#unregister-custom-client","text":"// unregister custom client factory registerCustomRequestClientFactory(null);","title":"Unregister Custom Client"},{"location":"v2/sp/custom-irequestclient/#irequestclient-interface","text":"If you want to 100% roll your own client you need to implement the below interface, found in common. import { IRequestClient } from \"@pnp/core\"; export interface IRequestClient { fetch(url: string, options?: IFetchOptions): Promise; fetchRaw(url: string, options?: IFetchOptions): Promise; get(url: string, options?: IFetchOptions): Promise; post(url: string, options?: IFetchOptions): Promise; patch(url: string, options?: IFetchOptions): Promise; delete(url: string, options?: IFetchOptions): Promise; }","title":"IRequestClient Interface"},{"location":"v2/sp/custom-irequestclient/#supportability-note","text":"We cannot provide support for your custom client implementation, and creating your own client assumes an intimate knowledge of how SharePoint requests work. Again, this is very likely something you will never need to do - and we recommend exhausting all other options before taking this route.","title":"Supportability Note"},{"location":"v2/sp/entity-merging/","text":"@pnp/sp - entity merging \u00b6 Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its representing type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples. Importing spODataEntity and spODataEntityArray \u00b6 You can import spODataEntity and spODataEntityArray in two ways, depending on your use case. The simplest way is to use the presets/all import as shown in the examples. The downside of this approach is that you can't take advantage of selective imports. If you want to take advantage of selective imports while using either of the entity parsers you can use: import { spODataEntity, spODataEntityArray } from \"@pnp/sp/odata\"; The full selective import for the first sample would be: import { sp } from \"@pnp/sp\"; import { spODataEntity } from \"@pnp/sp/odata\"; import { Item, IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; Request a single entity \u00b6 If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query. import { sp, spODataEntity, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; } try { // get a list item loaded with data and merged into an instance of Item const item = await sp.web.lists.getByTitle(\"ListTitle\").items.getById(1).usingParser(spODataEntity(Item))(); // log the item id, all properties specified in MyProps will be type checked Logger.write(`Item id: ${item.Id}`); // now we can call update because we have an instance of the Item type to work with as well await item.update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); } Request a collection \u00b6 The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method. import { sp, spODataEntityArray, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; Title: string; } try { // get a list item loaded with data and merged into an instance of Item const items = await sp.web.lists.getByTitle(\"OrderByList\").items.select(\"Id\", \"Title\").usingParser(spODataEntityArray(Item))(); Logger.write(`Item id: ${items.length}`); Logger.write(`Item id: ${items[0].Title}`); // now we can call update because we have an instance of the Item type to work with as well await items[0].update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); }","title":"@pnp/sp - entity merging"},{"location":"v2/sp/entity-merging/#pnpsp-entity-merging","text":"Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its representing type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples.","title":"@pnp/sp - entity merging"},{"location":"v2/sp/entity-merging/#importing-spodataentity-and-spodataentityarray","text":"You can import spODataEntity and spODataEntityArray in two ways, depending on your use case. The simplest way is to use the presets/all import as shown in the examples. The downside of this approach is that you can't take advantage of selective imports. If you want to take advantage of selective imports while using either of the entity parsers you can use: import { spODataEntity, spODataEntityArray } from \"@pnp/sp/odata\"; The full selective import for the first sample would be: import { sp } from \"@pnp/sp\"; import { spODataEntity } from \"@pnp/sp/odata\"; import { Item, IItem } from \"@pnp/sp/items\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\";","title":"Importing spODataEntity and spODataEntityArray"},{"location":"v2/sp/entity-merging/#request-a-single-entity","text":"If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query. import { sp, spODataEntity, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; } try { // get a list item loaded with data and merged into an instance of Item const item = await sp.web.lists.getByTitle(\"ListTitle\").items.getById(1).usingParser(spODataEntity(Item))(); // log the item id, all properties specified in MyProps will be type checked Logger.write(`Item id: ${item.Id}`); // now we can call update because we have an instance of the Item type to work with as well await item.update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); }","title":"Request a single entity"},{"location":"v2/sp/entity-merging/#request-a-collection","text":"The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method. import { sp, spODataEntityArray, Item, IItem } from \"@pnp/sp/presets/all\"; // interface defining the returned properties interface MyProps { Id: number; Title: string; } try { // get a list item loaded with data and merged into an instance of Item const items = await sp.web.lists.getByTitle(\"OrderByList\").items.select(\"Id\", \"Title\").usingParser(spODataEntityArray(Item))(); Logger.write(`Item id: ${items.length}`); Logger.write(`Item id: ${items[0].Title}`); // now we can call update because we have an instance of the Item type to work with as well await items[0].update({ Title: \"New title.\", }); } catch (e) { Logger.error(e); }","title":"Request a collection"},{"location":"v2/sp/features/","text":"@pnp/sp/features \u00b6 Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web. IFeatures \u00b6 Represents a collection of features. SharePoint Sites and Webs will have a collection of features Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\"; getById \u00b6 Gets the information about a feature for the given GUID import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; const webFeature = await sp.web.features.getById(webFeatureId)(); const siteFeatureId = \"guid-of-site-scope-feature\"; const siteFeature = await sp.site.features.getById(siteFeatureId)(); add \u00b6 Adds (activates) a feature at the Site or Web level import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.add(webFeatureId); // Activate with force res = await sp.web.features.add(webFeatureId, true); remove \u00b6 Removes and deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.remove(webFeatureId); // Deactivate with force res = await sp.web.features.remove(webFeatureId, true); IFeature \u00b6 Represents an instance of a SharePoint feature. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features, IFeature, Feature } from \"@pnp/sp/presets/all\"; deactivate \u00b6 Deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; sp.web.features.getById(webFeatureId).deactivate() // Deactivate with force sp.web.features.getById(webFeatureId).deactivate(true)","title":"@pnp/sp/features"},{"location":"v2/sp/features/#pnpspfeatures","text":"Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.","title":"@pnp/sp/features"},{"location":"v2/sp/features/#ifeatures","text":"Represents a collection of features. SharePoint Sites and Webs will have a collection of features Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features } from \"@pnp/sp/presets/all\";","title":"IFeatures"},{"location":"v2/sp/features/#getbyid","text":"Gets the information about a feature for the given GUID import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; const webFeature = await sp.web.features.getById(webFeatureId)(); const siteFeatureId = \"guid-of-site-scope-feature\"; const siteFeature = await sp.site.features.getById(siteFeatureId)();","title":"getById"},{"location":"v2/sp/features/#add","text":"Adds (activates) a feature at the Site or Web level import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.add(webFeatureId); // Activate with force res = await sp.web.features.add(webFeatureId, true);","title":"add"},{"location":"v2/sp/features/#remove","text":"Removes and deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; let res = await sp.web.features.remove(webFeatureId); // Deactivate with force res = await sp.web.features.remove(webFeatureId, true);","title":"remove"},{"location":"v2/sp/features/#ifeature","text":"Represents an instance of a SharePoint feature. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features/site\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features/web\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/features\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; Preset: All import { sp, IFeatures, Features, IFeature, Feature } from \"@pnp/sp/presets/all\";","title":"IFeature"},{"location":"v2/sp/features/#deactivate","text":"Deactivates the specified feature from the SharePoint Site or Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/features\"; //Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a const webFeatureId = \"guid-of-web-feature\"; sp.web.features.getById(webFeatureId).deactivate() // Deactivate with force sp.web.features.getById(webFeatureId).deactivate(true)","title":"deactivate"},{"location":"v2/sp/fields/","text":"@pnp/sp/lists \u00b6 Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list. IFields \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Fields, IFields } from \"@pnp/sp/fields\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; Preset: All import { sp, Fields, IFields } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Fields, IFields } from \"@pnp/sp/presets/core\"; Get Field by Id \u00b6 Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/fields\"; // get the field by Id for web const field: IField = sp.web.fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // get the field by Id for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\")(); // we can use this 'field' variable to execute more queries on the field: const r = await field.select(\"Title\")(); // show the response from the server console.log(r.Title); Get Field by Title \u00b6 You can also get a field from the collection by title. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the title 'Author' for web const field: IField = sp.web.fields.getByTitle(\"Author\"); // get the field with the title 'Author' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Author\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Get Field by Internal Name or Title \u00b6 You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the internal name 'ModifiedBy' for web const field: IField = sp.web.fields.getByInternalNameOrTitle(\"ModifiedBy\"); // get the field with the internal name 'ModifiedBy' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByInternalNameOrTitle(\"ModifiedBy\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Create a Field using an XML schema \u00b6 Create a new field by defining an XML schema that assigns all the properties for the field. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // define the schema for your new field, in this case a date field with a default date of today. const fieldSchema = `[today]`; // create the new field in the web const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema); // create the new field in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(fieldSchema); // we can use this 'field' variable to run more queries on the list: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a New Field \u00b6 Use the add method to create a new field where you define the field type import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // create a new field called 'My Field' in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Site Field to a List \u00b6 Use the createFieldAsXml method to add a site field to a list. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // add the site field 'My Field' to the list 'My List' const r = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(field.data.SchemaXml); // log the field Id to console console.log(r.data.Id); Add a Text Field \u00b6 Use the addText method to create a new text field. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new text field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // create a new text field called 'My Field' in the list 'My List'. const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Calculated Field \u00b6 Use the addCalculated method to create a new calculated field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, FieldTypes } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new calculated field called 'My Field' in web const field = await sp.web.fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // create a new calculated field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Date/Time Field \u00b6 Use the addDateTime method to create a new date/time field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new date/time field called 'My Field' in web const field = await sp.web.fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // create a new date/time field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Currency Field \u00b6 Use the addCurrency method to create a new currency field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new currency field called 'My Field' in web const field = await sp.web.fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // create a new currency field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Multi-line Text Field \u00b6 Use the addMultilineText method to create a new multi-line text field. For Enhanced Rich Text mode, see the next section. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new multi-line text field called 'My Field' in web const field = await sp.web.fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // create a new multi-line text field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Multi-line Text Field with Enhanced Rich Text \u00b6 The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; //Create a new multi-line text field called 'My Field' in web const field = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml( `` ); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Number Field \u00b6 Use the addNumber method to create a new number field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new number field called 'My Field' in web const field = await sp.web.fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // create a new number field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a URL Field \u00b6 Use the addUrl method to create a new url field. import { sp } from \"@pnp/sp\"; import { UrlFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new url field called 'My Field' in web const field = await sp.web.fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // create a new url field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a User Field \u00b6 Use the addUser method to create a new user field. import { sp } from \"@pnp/sp\"; import { FieldUserSelectionMode } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new user field called 'My Field' in web const field = await sp.web.fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // create a new user field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Lookup Field \u00b6 Use the addLookup method to create a new lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const list = await sp.web.lists.getByTitle(\"My Lookup List\")(); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web. const field = await sp.web.fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); // ** // Adding a lookup that supports multiple values takes two calls: const fieldAddResult = await sp.web.fields.addLookup(\"Test Lookup 124\", \"GUID\", \"Title\"); await fieldAddResult.field.update({ Description: 'New Description' }, \"SP.FieldLookup\"); Add a Choice Field \u00b6 Use the addChoice method to create a new choice field. import { sp } from \"@pnp/sp\"; import { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new choice field called 'My Field' in web const field = await sp.web.fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // create a new choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Multi-Choice Field \u00b6 Use the addMultiChoice method to create a new multi-choice field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new multi-choice field called 'My Field' in web const field = await sp.web.fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // create a new multi-choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Boolean Field \u00b6 Use the addBoolean method to create a new boolean field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new boolean field called 'My Field' in web const field = await sp.web.fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // create a new boolean field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Dependent Lookup Field \u00b6 Use the addDependentLookupField method to create a new dependent lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web. const field = await sp.web.fields.getByTitle(\"My Field\")(); const fieldDep = await sp.web.fields.addDependentLookupField(\"My Dep Field\", field.Id, \"Description\"); // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\")(); const fieldDep2 = await sp.web.lists.getByTitle(\"My List\").fields.addDependentLookupField(\"My Dep Field\", field2.Id, \"Description\"); // we can use this 'fieldDep' variable to run more queries on the field: const r = await fieldDep.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Add a Location Field \u00b6 Use the addLocation method to create a new location field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new location field called 'My Field' in web const field = await sp.web.fields.addLocation(\"My Field\", { Group: \"My Group\" }); // create a new location field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLocation(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); Delete a Field \u00b6 Use the delete method to delete a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; // delete one or more fields from web, returns boolean const result = await sp.web.fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.fields.getByTitle(\"My Field 2\").delete(); // delete one or more fields from list 'My List', returns boolean const result = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field 2\").delete(); Update a Field \u00b6 Use the update method to update a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // update the field called 'My Field' with a description in web, returns FieldUpdateResult const fieldUpdate = await sp.web.fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // if you need to update a field with properties for a specific field type you can optionally include the field type as a second param // if you do not include it we will look up the type, but that adds a call to the server const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Look up Field\").update({ RelationshipDeleteBehavior: 1 }, \"SP.FieldLookup\"); Show a Field in the Display Form \u00b6 Use the setShowInDisplayForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in display form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInDisplayForm(true); // show field called 'My Field' in display form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInDisplayForm(true); Show a Field in the Edit Form \u00b6 Use the setShowInEditForm method to add a field to the edit form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in edit form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInEditForm(true); // show field called 'My Field' in edit form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInEditForm(true); Show a Field in the New Form \u00b6 Use the setShowInNewForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in new form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInNewForm(true); // show field called 'My Field' in new form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInNewForm(true);","title":"@pnp/sp/lists"},{"location":"v2/sp/fields/#pnpsplists","text":"Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.","title":"@pnp/sp/lists"},{"location":"v2/sp/fields/#ifields","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Fields, IFields } from \"@pnp/sp/fields\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; Preset: All import { sp, Fields, IFields } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Fields, IFields } from \"@pnp/sp/presets/core\";","title":"IFields"},{"location":"v2/sp/fields/#get-field-by-id","text":"Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/fields\"; // get the field by Id for web const field: IField = sp.web.fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // get the field by Id for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\")(); // we can use this 'field' variable to execute more queries on the field: const r = await field.select(\"Title\")(); // show the response from the server console.log(r.Title);","title":"Get Field by Id"},{"location":"v2/sp/fields/#get-field-by-title","text":"You can also get a field from the collection by title. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the title 'Author' for web const field: IField = sp.web.fields.getByTitle(\"Author\"); // get the field with the title 'Author' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"Author\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Get Field by Title"},{"location":"v2/sp/fields/#get-field-by-internal-name-or-title","text":"You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\" import \"@pnp/sp/fields\"; // get the field with the internal name 'ModifiedBy' for web const field: IField = sp.web.fields.getByInternalNameOrTitle(\"ModifiedBy\"); // get the field with the internal name 'ModifiedBy' for list 'My List' const field2: IFieldInfo = await sp.web.lists.getByTitle(\"My List\").fields.getByInternalNameOrTitle(\"ModifiedBy\")(); // we can use this 'field' variable to run more queries on the field: const r = await field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Get Field by Internal Name or Title"},{"location":"v2/sp/fields/#create-a-field-using-an-xml-schema","text":"Create a new field by defining an XML schema that assigns all the properties for the field. import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // define the schema for your new field, in this case a date field with a default date of today. const fieldSchema = `[today]`; // create the new field in the web const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema); // create the new field in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(fieldSchema); // we can use this 'field' variable to run more queries on the list: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Create a Field using an XML schema"},{"location":"v2/sp/fields/#add-a-new-field","text":"Use the add method to create a new field where you define the field type import { sp } from \"@pnp/sp\"; import { IField } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // create a new field called 'My Field' in the list 'My List' const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a New Field"},{"location":"v2/sp/fields/#add-a-site-field-to-a-list","text":"Use the createFieldAsXml method to add a site field to a list. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.add(\"My Field\", \"SP.FieldText\", { FieldTypeKind: 3, Group: \"My Group\" }); // add the site field 'My Field' to the list 'My List' const r = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml(field.data.SchemaXml); // log the field Id to console console.log(r.data.Id);","title":"Add a Site Field to a List"},{"location":"v2/sp/fields/#add-a-text-field","text":"Use the addText method to create a new text field. import { sp } from \"@pnp/sp\"; import { IFieldAddResult } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new text field called 'My Field' in web. const field: IFieldAddResult = await sp.web.fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // create a new text field called 'My Field' in the list 'My List'. const field2: IFieldAddResult = await sp.web.lists.getByTitle(\"My List\").fields.addText(\"My Field\", 255, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Text Field"},{"location":"v2/sp/fields/#add-a-calculated-field","text":"Use the addCalculated method to create a new calculated field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, FieldTypes } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new calculated field called 'My Field' in web const field = await sp.web.fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // create a new calculated field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCalculated(\"My Field\", \"=Modified+1\", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: \"MyGroup\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Calculated Field"},{"location":"v2/sp/fields/#add-a-datetime-field","text":"Use the addDateTime method to create a new date/time field. import { sp } from \"@pnp/sp\"; import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new date/time field called 'My Field' in web const field = await sp.web.fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // create a new date/time field called 'My Field' in the list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addDateTime(\"My Field\", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Date/Time Field"},{"location":"v2/sp/fields/#add-a-currency-field","text":"Use the addCurrency method to create a new currency field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new currency field called 'My Field' in web const field = await sp.web.fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // create a new currency field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addCurrency(\"My Field\", 0, 100, 1033, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Currency Field"},{"location":"v2/sp/fields/#add-a-multi-line-text-field","text":"Use the addMultilineText method to create a new multi-line text field. For Enhanced Rich Text mode, see the next section. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new multi-line text field called 'My Field' in web const field = await sp.web.fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // create a new multi-line text field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultilineText(\"My Field\", 6, true, false, false, true, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Multi-line Text Field"},{"location":"v2/sp/fields/#add-a-multi-line-text-field-with-enhanced-rich-text","text":"The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; //Create a new multi-line text field called 'My Field' in web const field = await sp.web.lists.getByTitle(\"My List\").fields.createFieldAsXml( `` ); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Multi-line Text Field with Enhanced Rich Text"},{"location":"v2/sp/fields/#add-a-number-field","text":"Use the addNumber method to create a new number field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new number field called 'My Field' in web const field = await sp.web.fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // create a new number field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addNumber(\"My Field\", 1, 100, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Number Field"},{"location":"v2/sp/fields/#add-a-url-field","text":"Use the addUrl method to create a new url field. import { sp } from \"@pnp/sp\"; import { UrlFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new url field called 'My Field' in web const field = await sp.web.fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // create a new url field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUrl(\"My Field\", UrlFieldFormatType.Hyperlink, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a URL Field"},{"location":"v2/sp/fields/#add-a-user-field","text":"Use the addUser method to create a new user field. import { sp } from \"@pnp/sp\"; import { FieldUserSelectionMode } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new user field called 'My Field' in web const field = await sp.web.fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // create a new user field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addUser(\"My Field\", FieldUserSelectionMode.PeopleOnly, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a User Field"},{"location":"v2/sp/fields/#add-a-lookup-field","text":"Use the addLookup method to create a new lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const list = await sp.web.lists.getByTitle(\"My Lookup List\")(); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web. const field = await sp.web.fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLookup(\"My Field\", list.Id, \"Title\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id); // ** // Adding a lookup that supports multiple values takes two calls: const fieldAddResult = await sp.web.fields.addLookup(\"Test Lookup 124\", \"GUID\", \"Title\"); await fieldAddResult.field.update({ Description: 'New Description' }, \"SP.FieldLookup\");","title":"Add a Lookup Field"},{"location":"v2/sp/fields/#add-a-choice-field","text":"Use the addChoice method to create a new choice field. import { sp } from \"@pnp/sp\"; import { ChoiceFieldFormatType } from \"@pnp/sp/fields/types\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new choice field called 'My Field' in web const field = await sp.web.fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // create a new choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addChoice(\"My Field\", choices, ChoiceFieldFormatType.Dropdown, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Choice Field"},{"location":"v2/sp/fields/#add-a-multi-choice-field","text":"Use the addMultiChoice method to create a new multi-choice field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`]; // create a new multi-choice field called 'My Field' in web const field = await sp.web.fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // create a new multi-choice field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addMultiChoice(\"My Field\", choices, false, { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Multi-Choice Field"},{"location":"v2/sp/fields/#add-a-boolean-field","text":"Use the addBoolean method to create a new boolean field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new boolean field called 'My Field' in web const field = await sp.web.fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // create a new boolean field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addBoolean(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Boolean Field"},{"location":"v2/sp/fields/#add-a-dependent-lookup-field","text":"Use the addDependentLookupField method to create a new dependent lookup field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web. const field = await sp.web.fields.getByTitle(\"My Field\")(); const fieldDep = await sp.web.fields.addDependentLookupField(\"My Dep Field\", field.Id, \"Description\"); // create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\")(); const fieldDep2 = await sp.web.lists.getByTitle(\"My List\").fields.addDependentLookupField(\"My Dep Field\", field2.Id, \"Description\"); // we can use this 'fieldDep' variable to run more queries on the field: const r = await fieldDep.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Dependent Lookup Field"},{"location":"v2/sp/fields/#add-a-location-field","text":"Use the addLocation method to create a new location field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // create a new location field called 'My Field' in web const field = await sp.web.fields.addLocation(\"My Field\", { Group: \"My Group\" }); // create a new location field called 'My Field' in list 'My List' const field2 = await sp.web.lists.getByTitle(\"My List\").fields.addLocation(\"My Field\", { Group: \"My Group\" }); // we can use this 'field' variable to run more queries on the field: const r = await field.field.select(\"Id\")(); // log the field Id to console console.log(r.Id);","title":"Add a Location Field"},{"location":"v2/sp/fields/#delete-a-field","text":"Use the delete method to delete a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/fields\"; // delete one or more fields from web, returns boolean const result = await sp.web.fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.fields.getByTitle(\"My Field 2\").delete(); // delete one or more fields from list 'My List', returns boolean const result = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").delete(); const result2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field 2\").delete();","title":"Delete a Field"},{"location":"v2/sp/fields/#update-a-field","text":"Use the update method to update a field. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // update the field called 'My Field' with a description in web, returns FieldUpdateResult const fieldUpdate = await sp.web.fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").update({ Description: \"My Description\" }); // if you need to update a field with properties for a specific field type you can optionally include the field type as a second param // if you do not include it we will look up the type, but that adds a call to the server const fieldUpdate2 = await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Look up Field\").update({ RelationshipDeleteBehavior: 1 }, \"SP.FieldLookup\");","title":"Update a Field"},{"location":"v2/sp/fields/#show-a-field-in-the-display-form","text":"Use the setShowInDisplayForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in display form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInDisplayForm(true); // show field called 'My Field' in display form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInDisplayForm(true);","title":"Show a Field in the Display Form"},{"location":"v2/sp/fields/#show-a-field-in-the-edit-form","text":"Use the setShowInEditForm method to add a field to the edit form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in edit form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInEditForm(true); // show field called 'My Field' in edit form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInEditForm(true);","title":"Show a Field in the Edit Form"},{"location":"v2/sp/fields/#show-a-field-in-the-new-form","text":"Use the setShowInNewForm method to add a field to the display form. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/fields\"; // show field called 'My Field' in new form throughout web await sp.web.fields.getByTitle(\"My Field\").setShowInNewForm(true); // show field called 'My Field' in new form for list 'My List' await sp.web.lists.getByTitle(\"My List\").fields.getByTitle(\"My Field\").setShowInNewForm(true);","title":"Show a Field in the New Form"},{"location":"v2/sp/files/","text":"@pnp/sp/files \u00b6 One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below. Reading Files \u00b6 Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const blob: Blob = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBlob(); const buffer: ArrayBuffer = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBuffer(); const json: any = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.json\").getJSON(); const text: string = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.txt\").getText(); // all of these also work from a file object no matter how you access it const text2: string = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/documents\").files.getByName(\"file.txt\").getText(); getFileByUrl \u00b6 Added in 2.0.4 This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const url = \"{absolute file url OR sharing url}\"; // file is an IFile and supports all the file operations const file = sp.web.getFileByUrl(url); // for example const fileContent = await file.getText(); Adding Files \u00b6 Likewise you can add files using one of two methods, add or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. declare var require: (s: string) => any; import { ConsoleListener, Logger, LogLevel } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import { auth } from \"./auth\"; let $ = require(\"jquery\"); // <-- used here for illustration let siteUrl = \"https://mytenant.sharepoint.com/sites/dev\"; // comment this out for non-node execution // auth(siteUrl); Logger.subscribe(new ConsoleListener()); Logger.activeLogLevel = LogLevel.Verbose; let web = Web(siteUrl); $(() => { $(\"#testingdiv\").append(\"\"); $(\"#thebuttontodoit\").on('click', async (e) => { e.preventDefault(); let input = document.getElementById(\"thefileinput\"); let file = input.files[0]; // you can adjust this number to control what size files are uploaded in chunks if (file.size <= 10485760) { // small upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(file.name, file, true); Logger.write(\"done\"); } else { // large upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addChunked(file.name, file, data => { Logger.log({ data: data, level: LogLevel.Verbose, message: \"progress\" }); }, true); Logger.write(\"done!\") } }); }); Adding a file using Nodejs Streams \u00b6 If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams. // triggers auto-application of extensions, in this case to add getStream import \"@pnp/nodejs\"; // get a stream of an existing file const sr = await sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/old.md\").getStream(); // now add the stream as a new file, remember to set the content-length header const fr = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.configure({ headers: { \"content-length\": `${sr.knownLength}`, }, }).add(\"new.md\", sr.body); Setting Associated Item Values \u00b6 You can also update the file properties of a newly uploaded file using code similar to the below snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(\"file.name\", \"file\", true); const item = await file.file.getItem(); await item.update({ Title: \"A Title\", OtherField: \"My Other Value\" }); AddUsingPath \u00b6 If you need to support the percent or pound characters you can use the addUsingPath method of IFiles import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addUsingPath(\"file%#%.name\", \"content\"); Update File Content \u00b6 You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.txt\").setContent(\"New string content for the file.\"); await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.mp4\").setContentChunked(file); Check in, Check out, and Approve & Deny \u00b6 The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below. Check In \u00b6 Check in takes two optional arguments, comment and check in type. import { sp } from \"@pnp/sp\"; import { CheckinType } from \"@pnp/sp/files\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // default options with empty comment and CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(); console.log(\"File checked in!\"); // supply a comment (< 1024 chars) and using default check in type CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\"); console.log(\"File checked in!\"); // Supply both comment and check in type await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\", CheckinType.Overwrite); console.log(\"File checked in!\"); Check Out \u00b6 Check out takes no arguments. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkout(); console.log(\"File checked out!\"); Approve and Deny \u00b6 You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").approve(\"Approval Comment\"); console.log(\"File approved!\"); // deny with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(); console.log(\"File denied!\"); // deny with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(\"Deny comment\"); console.log(\"File denied!\"); Publish and Unpublish \u00b6 You can both publish and unpublish a file using the library. Both methods take an optional comment argument. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // publish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(); console.log(\"File published!\"); // publish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(\"Publish comment\"); console.log(\"File published!\"); // unpublish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(); console.log(\"File unpublished!\"); // unpublish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(\"Unpublish comment\"); console.log(\"File unpublished!\"); Advanced Upload Options \u00b6 Both the addChunked and setContentChunked methods support options beyond just supplying the file content. progress function \u00b6 A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: (data: ChunkedFileUploadProgressData) => void The data interface is: export interface ChunkedFileUploadProgressData { stage: \"starting\" | \"continue\" | \"finishing\"; blockNumber: number; totalBlocks: number; chunkSize: number; currentPointer: number; fileSize: number; } chunkSize \u00b6 This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts. getItem \u00b6 This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/security\"; const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(); console.log(item); const item2 = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(\"Title\", \"Modified\"); console.log(item2); // you can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms); You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/items\"; import \"@pnp/sp/security\"; // also supports typing the objects so your type will be a union type const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem<{ Id: number, Title: string }>(\"Id\", \"Title\"); // You get intellisense and proper typing of the returned object console.log(`Id: ${item.Id} -- ${item.Title}`); // You can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms); move \u00b6 It's possible to move a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveTo(destinationUrl); copy \u00b6 It's possible to copy a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyTo(destinationUrl, false); move by path \u00b6 It's possible to move a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveByPath(destinationUrl, false, true); copy by path \u00b6 It's possible to copy a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, true); getFileById \u00b6 You can get a file by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import { IFile } from \"@pnp/sp/files\"; const file: IFile = sp.web.getFileById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); delete \u00b6 Deletes a file import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").delete(); delete with params \u00b6 Added in 2.0.9 Deletes a file with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").deleteWithParams({ BypassSharedLock: true, }); exists \u00b6 Added in 2.0.9 Checks to see if a file exists import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; const exists = await sp.web.rootFolder.files.getByName(\"name.txt\").exists();","title":"@pnp/sp/files"},{"location":"v2/sp/files/#pnpspfiles","text":"One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.","title":"@pnp/sp/files"},{"location":"v2/sp/files/#reading-files","text":"Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser . import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const blob: Blob = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBlob(); const buffer: ArrayBuffer = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.avi\").getBuffer(); const json: any = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.json\").getJSON(); const text: string = await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/file.txt\").getText(); // all of these also work from a file object no matter how you access it const text2: string = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/documents\").files.getByName(\"file.txt\").getText();","title":"Reading Files"},{"location":"v2/sp/files/#getfilebyurl","text":"Added in 2.0.4 This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files/web\"; const url = \"{absolute file url OR sharing url}\"; // file is an IFile and supports all the file operations const file = sp.web.getFileByUrl(url); // for example const fileContent = await file.getText();","title":"getFileByUrl"},{"location":"v2/sp/files/#adding-files","text":"Likewise you can add files using one of two methods, add or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size. declare var require: (s: string) => any; import { ConsoleListener, Logger, LogLevel } from \"@pnp/logging\"; import { sp } from \"@pnp/sp\"; import { Web } from \"@pnp/sp/webs\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import { auth } from \"./auth\"; let $ = require(\"jquery\"); // <-- used here for illustration let siteUrl = \"https://mytenant.sharepoint.com/sites/dev\"; // comment this out for non-node execution // auth(siteUrl); Logger.subscribe(new ConsoleListener()); Logger.activeLogLevel = LogLevel.Verbose; let web = Web(siteUrl); $(() => { $(\"#testingdiv\").append(\"\"); $(\"#thebuttontodoit\").on('click', async (e) => { e.preventDefault(); let input = document.getElementById(\"thefileinput\"); let file = input.files[0]; // you can adjust this number to control what size files are uploaded in chunks if (file.size <= 10485760) { // small upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(file.name, file, true); Logger.write(\"done\"); } else { // large upload await web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addChunked(file.name, file, data => { Logger.log({ data: data, level: LogLevel.Verbose, message: \"progress\" }); }, true); Logger.write(\"done!\") } }); });","title":"Adding Files"},{"location":"v2/sp/files/#adding-a-file-using-nodejs-streams","text":"If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams. // triggers auto-application of extensions, in this case to add getStream import \"@pnp/nodejs\"; // get a stream of an existing file const sr = await sp.web.getFileByServerRelativePath(\"/sites/dev/shared documents/old.md\").getStream(); // now add the stream as a new file, remember to set the content-length header const fr = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.configure({ headers: { \"content-length\": `${sr.knownLength}`, }, }).add(\"new.md\", sr.body);","title":"Adding a file using Nodejs Streams"},{"location":"v2/sp/files/#setting-associated-item-values","text":"You can also update the file properties of a newly uploaded file using code similar to the below snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.add(\"file.name\", \"file\", true); const item = await file.file.getItem(); await item.update({ Title: \"A Title\", OtherField: \"My Other Value\" });","title":"Setting Associated Item Values"},{"location":"v2/sp/files/#addusingpath","text":"If you need to support the percent or pound characters you can use the addUsingPath method of IFiles import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; const file = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared%20Documents/test/\").files.addUsingPath(\"file%#%.name\", \"content\");","title":"AddUsingPath"},{"location":"v2/sp/files/#update-file-content","text":"You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.txt\").setContent(\"New string content for the file.\"); await sp.web.getFileByServerRelativeUrl(\"/sites/dev/documents/test.mp4\").setContentChunked(file);","title":"Update File Content"},{"location":"v2/sp/files/#check-in-check-out-and-approve-deny","text":"The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.","title":"Check in, Check out, and Approve & Deny"},{"location":"v2/sp/files/#check-in","text":"Check in takes two optional arguments, comment and check in type. import { sp } from \"@pnp/sp\"; import { CheckinType } from \"@pnp/sp/files\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // default options with empty comment and CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(); console.log(\"File checked in!\"); // supply a comment (< 1024 chars) and using default check in type CheckinType.Major await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\"); console.log(\"File checked in!\"); // Supply both comment and check in type await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkin(\"A comment\", CheckinType.Overwrite); console.log(\"File checked in!\");","title":"Check In"},{"location":"v2/sp/files/#check-out","text":"Check out takes no arguments. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").checkout(); console.log(\"File checked out!\");","title":"Check Out"},{"location":"v2/sp/files/#approve-and-deny","text":"You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").approve(\"Approval Comment\"); console.log(\"File approved!\"); // deny with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(); console.log(\"File denied!\"); // deny with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").deny(\"Deny comment\"); console.log(\"File denied!\");","title":"Approve and Deny"},{"location":"v2/sp/files/#publish-and-unpublish","text":"You can both publish and unpublish a file using the library. Both methods take an optional comment argument. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // publish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(); console.log(\"File published!\"); // publish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").publish(\"Publish comment\"); console.log(\"File published!\"); // unpublish with no comment await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(); console.log(\"File unpublished!\"); // unpublish with a supplied comment. await sp.web.getFileByServerRelativeUrl(\"/sites/dev/shared documents/file.txt\").unpublish(\"Unpublish comment\"); console.log(\"File unpublished!\");","title":"Publish and Unpublish"},{"location":"v2/sp/files/#advanced-upload-options","text":"Both the addChunked and setContentChunked methods support options beyond just supplying the file content.","title":"Advanced Upload Options"},{"location":"v2/sp/files/#progress-function","text":"A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature: (data: ChunkedFileUploadProgressData) => void The data interface is: export interface ChunkedFileUploadProgressData { stage: \"starting\" | \"continue\" | \"finishing\"; blockNumber: number; totalBlocks: number; chunkSize: number; currentPointer: number; fileSize: number; }","title":"progress function"},{"location":"v2/sp/files/#chunksize","text":"This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.","title":"chunkSize"},{"location":"v2/sp/files/#getitem","text":"This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/security\"; const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(); console.log(item); const item2 = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem(\"Title\", \"Modified\"); console.log(item2); // you can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms); You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/items\"; import \"@pnp/sp/security\"; // also supports typing the objects so your type will be a union type const item = await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.txt\").getItem<{ Id: number, Title: string }>(\"Id\", \"Title\"); // You get intellisense and proper typing of the returned object console.log(`Id: ${item.Id} -- ${item.Title}`); // You can also chain directly off this item instance const perms = await item.getCurrentUserEffectivePermissions(); console.log(perms);","title":"getItem"},{"location":"v2/sp/files/#move","text":"It's possible to move a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveTo(destinationUrl);","title":"move"},{"location":"v2/sp/files/#copy","text":"It's possible to copy a file to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyTo(destinationUrl, false);","title":"copy"},{"location":"v2/sp/files/#move-by-path","text":"It's possible to move a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").moveByPath(destinationUrl, false, true);","title":"move by path"},{"location":"v2/sp/files/#copy-by-path","text":"It's possible to copy a file to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; // destination is a server-relative url of a new file const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`; await sp.web.getFileByServerRelativePath(\"/sites/dev/Shared Documents/test.docx\").copyByPath(destinationUrl, false, true);","title":"copy by path"},{"location":"v2/sp/files/#getfilebyid","text":"You can get a file by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; import { IFile } from \"@pnp/sp/files\"; const file: IFile = sp.web.getFileById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");","title":"getFileById"},{"location":"v2/sp/files/#delete","text":"Deletes a file import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").delete();","title":"delete"},{"location":"v2/sp/files/#delete-with-params","text":"Added in 2.0.9 Deletes a file with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; await sp.web.rootFolder.files.getByName(\"name.txt\").deleteWithParams({ BypassSharedLock: true, });","title":"delete with params"},{"location":"v2/sp/files/#exists","text":"Added in 2.0.9 Checks to see if a file exists import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/files\"; const exists = await sp.web.rootFolder.files.getByName(\"name.txt\").exists();","title":"exists"},{"location":"v2/sp/folders/","text":"@pnp/sp/folders \u00b6 Folders serve as a container for your files and list items. IFolders \u00b6 Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\"; Get folders collection for various SharePoint objects \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; // gets web's folders const webFolders = await sp.web.folders(); // gets list's folders const listFolders = await sp.web.lists.getByTitle(\"My List\").rootFolder.folders(); // gets item's folders const itemFolders = await sp.web.lists.getByTitle(\"My List\").items.getById(1).folder.folders(); add \u00b6 Adds a new folder to collection of folders import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // creates a new folder for web with specified url const folderAddResult = await sp.web.folders.add(\"folder url\"); getByName \u00b6 Gets a folder instance from a collection by folder's name import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = await sp.web.folders.getByName(\"folder name\")(); IFolder \u00b6 Represents an instance of a SharePoint folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\"; Get a folder object associated with different SharePoint artifacts (web, list, list item) \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // web's folder const rootFolder = await sp.web.rootFolder(); // list's folder const listRootFolder = await sp.web.lists.getByTitle(\"234\").rootFolder(); // item's folder const itemFolder = await sp.web.lists.getByTitle(\"234\").items.getById(1).folder(); getItem \u00b6 Gets list item associated with a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folderItem = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").getItem(); move \u00b6 It's possible to move a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveTo(destinationUrl); copy \u00b6 It's possible to copy a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyTo(destinationUrl); move by path \u00b6 It's possible to move a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveByPath(destinationUrl, true); copy by path \u00b6 It's possible to copy a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyByPath(destinationUrl, true); delete \u00b6 Deletes a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").delete(); delete with params \u00b6 Added in 2.0.9 Deletes a folder with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").deleteWithParams({ BypassSharedLock: true, DeleteIfEmpty: true, }); recycle \u00b6 Recycles a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").recycle(); serverRelativeUrl \u00b6 Gets folder's server relative url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const relUrl = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").serverRelativeUrl(); update \u00b6 Updates folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").update({ \"Name\": \"New name\", }); contentTypeOrder \u00b6 Gets content type order of a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const order = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").contentTypeOrder(); folders \u00b6 Gets all child folders associated with the current folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folders = await sp.web.rootFolder.folders(); files \u00b6 Gets all files inside a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files/folder\"; const files = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files(); listItemAllFields \u00b6 Gets this folder's list item field values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const itemFields = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").listItemAllFields(); parentFolder \u00b6 Gets the parent folder, if available import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const parentFolder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").parentFolder(); properties \u00b6 Gets this folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const properties = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").properties(); uniqueContentTypeOrder \u00b6 Gets a value that specifies the content type order. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const contentTypeOrder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").uniqueContentTypeOrder(); Rename a folder \u00b6 You can rename a folder by updating FileLeafRef property: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\"); const item = await folder.getItem(); const result = await item.update({ FileLeafRef: \"Folder2\" }); Create a folder with custom content type \u00b6 Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; const newFolderResult = await sp.web.rootFolder.folders.getByName(\"Shared Documents\").folders.add(\"My New Folder\"); const item = await newFolderResult.folder.listItemAllFields(); await sp.web.lists.getByTitle(\"Documents\").items.getById(item.ID).update({ ContentTypeId: \"0x0120001E76ED75A3E3F3408811F0BF56C4CDDD\", MyFolderField: \"field value\", Title: \"My New Folder\", }); addSubFolderUsingPath \u00b6 Added in 2.0.9 You can use the addSubFolderUsingPath method to add a folder with some special chars supported import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; // add a folder to site assets const folder: IFolder = await web.rootFolder.folders.getByName(\"SiteAssets\").addSubFolderUsingPath(\"folder name\"); getFolderById \u00b6 You can get a folder by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); getParentInfos \u00b6 Added in 2.0.12 Gets information about folder, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); await folder.getParentInfos();","title":"@pnp/sp/folders"},{"location":"v2/sp/folders/#pnpspfolders","text":"Folders serve as a container for your files and list items.","title":"@pnp/sp/folders"},{"location":"v2/sp/folders/#ifolders","text":"Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\";","title":"IFolders"},{"location":"v2/sp/folders/#get-folders-collection-for-various-sharepoint-objects","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; // gets web's folders const webFolders = await sp.web.folders(); // gets list's folders const listFolders = await sp.web.lists.getByTitle(\"My List\").rootFolder.folders(); // gets item's folders const itemFolders = await sp.web.lists.getByTitle(\"My List\").items.getById(1).folder.folders();","title":"Get folders collection for various SharePoint objects"},{"location":"v2/sp/folders/#add","text":"Adds a new folder to collection of folders import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // creates a new folder for web with specified url const folderAddResult = await sp.web.folders.add(\"folder url\");","title":"add"},{"location":"v2/sp/folders/#getbyname","text":"Gets a folder instance from a collection by folder's name import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = await sp.web.folders.getByName(\"folder name\")();","title":"getByName"},{"location":"v2/sp/folders/#ifolder","text":"Represents an instance of a SharePoint folder. Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IFolders, Folders } from \"@pnp/sp/folders\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/web\"; Selective 4 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; Selective 5 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders/list\"; import \"@pnp/sp/folders/item\"; Preset: All import { sp, IFolders, Folders } from \"@pnp/sp/presets/all\";","title":"IFolder"},{"location":"v2/sp/folders/#get-a-folder-object-associated-with-different-sharepoint-artifacts-web-list-list-item","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // web's folder const rootFolder = await sp.web.rootFolder(); // list's folder const listRootFolder = await sp.web.lists.getByTitle(\"234\").rootFolder(); // item's folder const itemFolder = await sp.web.lists.getByTitle(\"234\").items.getById(1).folder();","title":"Get a folder object associated with different SharePoint artifacts (web, list, list item)"},{"location":"v2/sp/folders/#getitem","text":"Gets list item associated with a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folderItem = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").getItem();","title":"getItem"},{"location":"v2/sp/folders/#move","text":"It's possible to move a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveTo(destinationUrl);","title":"move"},{"location":"v2/sp/folders/#copy","text":"It's possible to copy a folder to a new destination within a site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyTo(destinationUrl);","title":"copy"},{"location":"v2/sp/folders/#move-by-path","text":"It's possible to move a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").moveByPath(destinationUrl, true);","title":"move by path"},{"location":"v2/sp/folders/#copy-by-path","text":"It's possible to copy a folder to a new destination within the same or a different site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; // destination is a server-relative url of a new folder const destinationUrl = `/sites/my-site/SiteAssets/new-folder`; await sp.web.rootFolder.folders.getByName(\"SiteAssets\").folders.getByName(\"My Folder\").copyByPath(destinationUrl, true);","title":"copy by path"},{"location":"v2/sp/folders/#delete","text":"Deletes a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").delete();","title":"delete"},{"location":"v2/sp/folders/#delete-with-params","text":"Added in 2.0.9 Deletes a folder with options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").deleteWithParams({ BypassSharedLock: true, DeleteIfEmpty: true, });","title":"delete with params"},{"location":"v2/sp/folders/#recycle","text":"Recycles a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.rootFolder.folders.getByName(\"My Folder\").recycle();","title":"recycle"},{"location":"v2/sp/folders/#serverrelativeurl","text":"Gets folder's server relative url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const relUrl = await sp.web.rootFolder.folders.getByName(\"SiteAssets\").serverRelativeUrl();","title":"serverRelativeUrl"},{"location":"v2/sp/folders/#update","text":"Updates folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").update({ \"Name\": \"New name\", });","title":"update"},{"location":"v2/sp/folders/#contenttypeorder","text":"Gets content type order of a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const order = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").contentTypeOrder();","title":"contentTypeOrder"},{"location":"v2/sp/folders/#folders","text":"Gets all child folders associated with the current folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folders = await sp.web.rootFolder.folders();","title":"folders"},{"location":"v2/sp/folders/#files","text":"Gets all files inside a folder import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files/folder\"; const files = await sp.web.getFolderByServerRelativePath(\"Shared Documents\").files();","title":"files"},{"location":"v2/sp/folders/#listitemallfields","text":"Gets this folder's list item field values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const itemFields = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").listItemAllFields();","title":"listItemAllFields"},{"location":"v2/sp/folders/#parentfolder","text":"Gets the parent folder, if available import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const parentFolder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\").parentFolder();","title":"parentFolder"},{"location":"v2/sp/folders/#properties","text":"Gets this folder's properties import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const properties = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").properties();","title":"properties"},{"location":"v2/sp/folders/#uniquecontenttypeorder","text":"Gets a value that specifies the content type order. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const contentTypeOrder = await sp.web.getFolderByServerRelativePath(\"Shared Documents/Folder2\").uniqueContentTypeOrder();","title":"uniqueContentTypeOrder"},{"location":"v2/sp/folders/#rename-a-folder","text":"You can rename a folder by updating FileLeafRef property: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder = sp.web.getFolderByServerRelativePath(\"Shared Documents/My Folder\"); const item = await folder.getItem(); const result = await item.update({ FileLeafRef: \"Folder2\" });","title":"Rename a folder"},{"location":"v2/sp/folders/#create-a-folder-with-custom-content-type","text":"Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/lists\"; const newFolderResult = await sp.web.rootFolder.folders.getByName(\"Shared Documents\").folders.add(\"My New Folder\"); const item = await newFolderResult.folder.listItemAllFields(); await sp.web.lists.getByTitle(\"Documents\").items.getById(item.ID).update({ ContentTypeId: \"0x0120001E76ED75A3E3F3408811F0BF56C4CDDD\", MyFolderField: \"field value\", Title: \"My New Folder\", });","title":"Create a folder with custom content type"},{"location":"v2/sp/folders/#addsubfolderusingpath","text":"Added in 2.0.9 You can use the addSubFolderUsingPath method to add a folder with some special chars supported import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; // add a folder to site assets const folder: IFolder = await web.rootFolder.folders.getByName(\"SiteAssets\").addSubFolderUsingPath(\"folder name\");","title":"addSubFolderUsingPath"},{"location":"v2/sp/folders/#getfolderbyid","text":"You can get a folder by Id from a web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\");","title":"getFolderById"},{"location":"v2/sp/folders/#getparentinfos","text":"Added in 2.0.12 Gets information about folder, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/folders\"; const folder: IFolder = sp.web.getFolderById(\"2b281c7b-ece9-4b76-82f9-f5cf5e152ba0\"); await folder.getParentInfos();","title":"getParentInfos"},{"location":"v2/sp/forms/","text":"@pnp/sp/forms \u00b6 Forms in SharePoint are the Display, New, and Edit forms associated with a list. IFields \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\"; Get Form by Id \u00b6 Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\"; // get the field by Id for web const form = sp.web.lists.getByTitle(\"Documents\").forms.getById(\"{c4486774-f1e2-4804-96f3-91edf3e22a19}\")();","title":"@pnp/sp/forms"},{"location":"v2/sp/forms/#pnpspforms","text":"Forms in SharePoint are the Display, New, and Edit forms associated with a list.","title":"@pnp/sp/forms"},{"location":"v2/sp/forms/#ifields","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\";","title":"IFields"},{"location":"v2/sp/forms/#get-form-by-id","text":"Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/forms\"; import \"@pnp/sp/lists\"; // get the field by Id for web const form = sp.web.lists.getByTitle(\"Documents\").forms.getById(\"{c4486774-f1e2-4804-96f3-91edf3e22a19}\")();","title":"Get Form by Id"},{"location":"v2/sp/hubsites/","text":"@pnp/sp/hubsites \u00b6 This module helps you with working with hub sites in your tenant. IHubSites \u00b6 Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/hubsites\"; Preset: All import { sp, HubSites, IHubSites } from \"@pnp/sp/presets/all\"; Get a Listing of All Hub sites \u00b6 import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; // invoke the hub sites object const hubsites: IHubSiteInfo[] = await sp.hubSites(); // you can also use select to only return certain fields: const hubsites2: IHubSiteInfo[] = await sp.hubSites.select(\"ID\", \"Title\", \"RelatedHubSiteIds\")(); Get Hub site by Id \u00b6 Using the getById method on the hubsites module to get a hub site by site Id (guid). import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; const hubsite: IHubSiteInfo = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\")(); // log hub site title to console console.log(hubsite.Title); Get ISite instance \u00b6 We provide a helper method to load the ISite instance from the HubSite import { sp } from \"@pnp/sp\"; import { ISite } from \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites\"; const site: ISite = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\").getSite(); const siteData = await site(); console.log(siteData.Title); Get Hub site data for a web \u00b6 import { sp } from \"@pnp/sp\"; import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; const webData: Partial = await sp.web.hubSiteData(); // you can also force a refresh of the hub site data const webData2: Partial = await sp.web.hubSiteData(true); syncHubSiteTheme \u00b6 Allows you to apply theme updates from the parent hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; await sp.web.syncHubSiteTheme(); Hub site Site Methods \u00b6 You manage hub sites at the Site level. joinHubSite \u00b6 Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // join a site to a hub site await sp.site.joinHubSite(\"{parent hub site id}\"); // remove a site from a hub site await sp.site.joinHubSite(\"00000000-0000-0000-0000-000000000000\"); registerHubSite \u00b6 Registers the current site collection as hub site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // register current site as a hub site await sp.site.registerHubSite(); unRegisterHubSite \u00b6 Un-registers the current site collection as hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // make a site no longer a hub await sp.site.unRegisterHubSite();","title":"@pnp/sp/hubsites"},{"location":"v2/sp/hubsites/#pnpsphubsites","text":"This module helps you with working with hub sites in your tenant.","title":"@pnp/sp/hubsites"},{"location":"v2/sp/hubsites/#ihubsites","text":"Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/hubsites\"; Preset: All import { sp, HubSites, IHubSites } from \"@pnp/sp/presets/all\";","title":"IHubSites"},{"location":"v2/sp/hubsites/#get-a-listing-of-all-hub-sites","text":"import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; // invoke the hub sites object const hubsites: IHubSiteInfo[] = await sp.hubSites(); // you can also use select to only return certain fields: const hubsites2: IHubSiteInfo[] = await sp.hubSites.select(\"ID\", \"Title\", \"RelatedHubSiteIds\")();","title":"Get a Listing of All Hub sites"},{"location":"v2/sp/hubsites/#get-hub-site-by-id","text":"Using the getById method on the hubsites module to get a hub site by site Id (guid). import { sp } from \"@pnp/sp\"; import { IHubSiteInfo } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/hubsites\"; const hubsite: IHubSiteInfo = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\")(); // log hub site title to console console.log(hubsite.Title);","title":"Get Hub site by Id"},{"location":"v2/sp/hubsites/#get-isite-instance","text":"We provide a helper method to load the ISite instance from the HubSite import { sp } from \"@pnp/sp\"; import { ISite } from \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites\"; const site: ISite = await sp.hubSites.getById(\"3504348e-b2be-49fb-a2a9-2d748db64beb\").getSite(); const siteData = await site(); console.log(siteData.Title);","title":"Get ISite instance"},{"location":"v2/sp/hubsites/#get-hub-site-data-for-a-web","text":"import { sp } from \"@pnp/sp\"; import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; const webData: Partial = await sp.web.hubSiteData(); // you can also force a refresh of the hub site data const webData2: Partial = await sp.web.hubSiteData(true);","title":"Get Hub site data for a web"},{"location":"v2/sp/hubsites/#synchubsitetheme","text":"Allows you to apply theme updates from the parent hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/hubsites/web\"; await sp.web.syncHubSiteTheme();","title":"syncHubSiteTheme"},{"location":"v2/sp/hubsites/#hub-site-site-methods","text":"You manage hub sites at the Site level.","title":"Hub site Site Methods"},{"location":"v2/sp/hubsites/#joinhubsite","text":"Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // join a site to a hub site await sp.site.joinHubSite(\"{parent hub site id}\"); // remove a site from a hub site await sp.site.joinHubSite(\"00000000-0000-0000-0000-000000000000\");","title":"joinHubSite"},{"location":"v2/sp/hubsites/#registerhubsite","text":"Registers the current site collection as hub site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // register current site as a hub site await sp.site.registerHubSite();","title":"registerHubSite"},{"location":"v2/sp/hubsites/#unregisterhubsite","text":"Un-registers the current site collection as hub site collection. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import \"@pnp/sp/hubsites/site\"; // make a site no longer a hub await sp.site.unRegisterHubSite();","title":"unRegisterHubSite"},{"location":"v2/sp/items/","text":"@pnp/sp/items \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\"; GET \u00b6 Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions. Basic Get \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // get all the items from a list const items: any[] = await sp.web.lists.getByTitle(\"My List\").items(); console.log(items); // get a specific item by id. const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); console.log(item); // use odata operators for more efficient queries const items2: any[] = await sp.web.lists.getByTitle(\"My List\").items.select(\"Title\", \"Description\").top(5).orderBy(\"Modified\", true)(); console.log(items2); Get Paged Items \u00b6 Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic case to get paged items form a list let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged(); // you can also provide a type for the returned values instead of any let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged<{Title: string}[]>(); // the query also works with select to choose certain fields and top to set the page size let items = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\", \"Description\").top(50).getPaged<{Title: string}[]>(); // the results object will have two properties and one method: // the results property will be an array of the items returned if (items.results.length > 0) { console.log(\"We got results!\"); for (let i = 0; i < items.results.length; i++) { // type checking works here if we specify the return type console.log(items.results[i].Title); } } // the hasNext property is used with the getNext method to handle paging // hasNext will be true so long as there are additional results if (items.hasNext) { // this will carry over the type specified in the original query for the results array items = await items.getNext(); console.log(items.results.length); } getListItemChangesSinceToken \u00b6 The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // Using RowLimit. Enables paging let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({RowLimit: '5'}); // Use QueryOptions to make a XML-style query. // Because it's XML we need to escape special characters // Instead of & we use & in the query let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({QueryOptions: ''}); // Get everything. Using null with ChangeToken gets everything let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({ChangeToken: null}); Get All Items \u00b6 Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic usage const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(); console.log(allItems.length); // set page size const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(4000); console.log(allItems.length); // use select and top. top will set page size and override the any value passed to getAll const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").top(4000).getAll(); console.log(allItems.length); // we can also use filter as a supported odata operation, but this will likely fail on large lists const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").filter(\"Title eq 'Test'\").getAll(); console.log(allItems.length); Retrieving Lookup Fields \u00b6 When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const items = await sp.web.lists.getByTitle(\"LookupList\").items.select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(items); const item = await sp.web.lists.getByTitle(\"LookupList\").items.getById(1).select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(item); Filter using Metadata fields \u00b6 To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; const r = await sp.web.lists.getByTitle(\"TaxonomyList\").getItemsByCAMLQuery({ ViewXml: `Term 2`, }); Retrieving PublishingPageImage \u00b6 The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread . Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { Web } from \"@pnp/sp/webs\"; try { const w = Web(\"https://{publishing site url}\"); const r = await w.lists.getByTitle(\"Pages\").items .select(\"Title\", \"FileRef\", \"FieldValuesAsText/MetaInfo\") .expand(\"FieldValuesAsText\") (); // look through the returned items. for (var i = 0; i < r.length; i++) { // the title field value console.log(r[i].Title); // find the value in the MetaInfo string using regex const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig.exec(r[i].FieldValuesAsText.MetaInfo); if (matches !== null && matches.length > 1) { // this wil be the value of the PublishingPageImage field console.log(matches[1]); } } } catch (e) { console.error(e); } Add Items \u00b6 There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { IItemAddResult } from \"@pnp/sp/items\"; // add an item to the list const iar: IItemAddResult = await sp.web.lists.getByTitle(\"My List\").items.add({ Title: \"Title\", Description: \"Description\" }); console.log(iar); Content Type \u00b6 You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; await sp.web.lists.getById(\"4D5A36EA-6E84-4160-8458-65C436DB765C\").items.add({ Title: \"Test 1\", ContentTypeId: \"0x01030058FD86C279252341AB303852303E4DAF\" }); User Fields \u00b6 There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a \"results\" property and an array. Examples for both are shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; const i = await sp.web.lists.getByTitle(\"PeopleFields\").items.add({ Title: getGUID(), User1Id: 9, // allows a single user User2Id: { results: [16, 45] // allows multiple users } }); console.log(i); If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array. import { sp } from \"@pnp/sp\"; const result = await sp.web.lists.getByTitle(\"UserFieldList\").items.getById(1).validateUpdateListItem([{ FieldName: \"UserField\", FieldValue: JSON.stringify([{ \"Key\": \"i:0#.f|membership|person@tenant.com\" }]), }, { FieldName: \"Title\", FieldValue: \"Test - Updated\", }]); Lookup Fields \u00b6 What is said for User Fields is, in general, relevant to Lookup Fields: Lookup Field types: Single-valued lookup Multiple-valued lookup Id suffix should be appended to the end of lookups EntityPropertyName in payloads Numeric Ids for lookups' items should be passed as values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; await sp.web.lists.getByTitle(\"LookupFields\").items.add({ Title: getGUID(), LookupFieldId: 2, // allows a single lookup value MultiLookupFieldId: { results: [ 1, 56 ] // allows multiple lookup value } }); Add Multiple Items \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidadd\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: \"Batch 6\" }, entityTypeFullName).then(b => { console.log(b); }); list.items.inBatch(batch).add({ Title: \"Batch 7\" }, entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\"); Update \u00b6 The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const i = await list.items.getById(1).update({ Title: \"My New Title\", Description: \"Here is a new description\" }); console.log(i); Getting and updating a collection using filter \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // you are getting back a collection here const items: any[] = await sp.web.lists.getByTitle(\"MyList\").items.top(1).filter(\"Title eq 'A Title'\")(); // see if we got something if (items.length > 0) { const updatedItem = await sp.web.lists.getByTitle(\"MyList\").items.getById(items[0].Id).update({ Title: \"Updated Title\", }); console.log(JSON.stringify(updatedItem)); } Update Multiple Items \u00b6 This approach avoids multiple calls for the same list's entity type name. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidupdate\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); // note requirement of \"*\" eTag param - or use a specific eTag value as needed list.items.getById(1).inBatch(batch).update({ Title: \"Batch 6\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); list.items.getById(2).inBatch(batch).update({ Title: \"Batch 7\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\") Recycle \u00b6 To send an item to the recycle bin use recycle. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const recycleBinIdentifier = await list.items.getById(1).recycle(); Delete \u00b6 Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).delete(); Delete With Params \u00b6 Added in 2.0.9 Deletes the item object with options. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).deleteWithParams({ BypassSharedLock: true, }); The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true Resolving field names \u00b6 It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used. The easiest way to get know EntityPropertyName is to use the following snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import \"@pnp/sp/fields\"; const response = await sp.web.lists .getByTitle('[Lists_Title]') .fields .select('Title, EntityPropertyName') .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) (); console.log(response.map(field => { return { Title: field.Title, EntityPropertyName: field.EntityPropertyName }; })); Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used. getParentInfos \u00b6 Added in 2.0.12 Gets information about an item, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); await item.getParentInfos();","title":"@pnp/sp/items"},{"location":"v2/sp/items/#pnpspitems","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\";","title":"@pnp/sp/items"},{"location":"v2/sp/items/#get","text":"Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.","title":"GET"},{"location":"v2/sp/items/#basic-get","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // get all the items from a list const items: any[] = await sp.web.lists.getByTitle(\"My List\").items(); console.log(items); // get a specific item by id. const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); console.log(item); // use odata operators for more efficient queries const items2: any[] = await sp.web.lists.getByTitle(\"My List\").items.select(\"Title\", \"Description\").top(5).orderBy(\"Modified\", true)(); console.log(items2);","title":"Basic Get"},{"location":"v2/sp/items/#get-paged-items","text":"Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic case to get paged items form a list let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged(); // you can also provide a type for the returned values instead of any let items = await sp.web.lists.getByTitle(\"BigList\").items.getPaged<{Title: string}[]>(); // the query also works with select to choose certain fields and top to set the page size let items = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\", \"Description\").top(50).getPaged<{Title: string}[]>(); // the results object will have two properties and one method: // the results property will be an array of the items returned if (items.results.length > 0) { console.log(\"We got results!\"); for (let i = 0; i < items.results.length; i++) { // type checking works here if we specify the return type console.log(items.results[i].Title); } } // the hasNext property is used with the getNext method to handle paging // hasNext will be true so long as there are additional results if (items.hasNext) { // this will carry over the type specified in the original query for the results array items = await items.getNext(); console.log(items.results.length); }","title":"Get Paged Items"},{"location":"v2/sp/items/#getlistitemchangessincetoken","text":"The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // Using RowLimit. Enables paging let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({RowLimit: '5'}); // Use QueryOptions to make a XML-style query. // Because it's XML we need to escape special characters // Instead of & we use & in the query let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({QueryOptions: ''}); // Get everything. Using null with ChangeToken gets everything let changes = await sp.web.lists.getByTitle(\"BigList\").getListItemChangesSinceToken({ChangeToken: null});","title":"getListItemChangesSinceToken"},{"location":"v2/sp/items/#get-all-items","text":"Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // basic usage const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(); console.log(allItems.length); // set page size const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.getAll(4000); console.log(allItems.length); // use select and top. top will set page size and override the any value passed to getAll const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").top(4000).getAll(); console.log(allItems.length); // we can also use filter as a supported odata operation, but this will likely fail on large lists const allItems: any[] = await sp.web.lists.getByTitle(\"BigList\").items.select(\"Title\").filter(\"Title eq 'Test'\").getAll(); console.log(allItems.length);","title":"Get All Items"},{"location":"v2/sp/items/#retrieving-lookup-fields","text":"When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; const items = await sp.web.lists.getByTitle(\"LookupList\").items.select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(items); const item = await sp.web.lists.getByTitle(\"LookupList\").items.getById(1).select(\"Title\", \"Lookup/Title\", \"Lookup/ID\").expand(\"Lookup\")(); console.log(item);","title":"Retrieving Lookup Fields"},{"location":"v2/sp/items/#filter-using-metadata-fields","text":"To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; const r = await sp.web.lists.getByTitle(\"TaxonomyList\").getItemsByCAMLQuery({ ViewXml: `Term 2`, });","title":"Filter using Metadata fields"},{"location":"v2/sp/items/#retrieving-publishingpageimage","text":"The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread . Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { Web } from \"@pnp/sp/webs\"; try { const w = Web(\"https://{publishing site url}\"); const r = await w.lists.getByTitle(\"Pages\").items .select(\"Title\", \"FileRef\", \"FieldValuesAsText/MetaInfo\") .expand(\"FieldValuesAsText\") (); // look through the returned items. for (var i = 0; i < r.length; i++) { // the title field value console.log(r[i].Title); // find the value in the MetaInfo string using regex const matches = /PublishingPageImage:SW\\|(.*?)\\r\\n/ig.exec(r[i].FieldValuesAsText.MetaInfo); if (matches !== null && matches.length > 1) { // this wil be the value of the PublishingPageImage field console.log(matches[1]); } } } catch (e) { console.error(e); }","title":"Retrieving PublishingPageImage"},{"location":"v2/sp/items/#add-items","text":"There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { IItemAddResult } from \"@pnp/sp/items\"; // add an item to the list const iar: IItemAddResult = await sp.web.lists.getByTitle(\"My List\").items.add({ Title: \"Title\", Description: \"Description\" }); console.log(iar);","title":"Add Items"},{"location":"v2/sp/items/#content-type","text":"You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation . While this documentation references SharePoint 2010 the structure of the IDs has not changed. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; await sp.web.lists.getById(\"4D5A36EA-6E84-4160-8458-65C436DB765C\").items.add({ Title: \"Test 1\", ContentTypeId: \"0x01030058FD86C279252341AB303852303E4DAF\" });","title":"Content Type"},{"location":"v2/sp/items/#user-fields","text":"There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with \"Id\" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id. Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a \"results\" property and an array. Examples for both are shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; const i = await sp.web.lists.getByTitle(\"PeopleFields\").items.add({ Title: getGUID(), User1Id: 9, // allows a single user User2Id: { results: [16, 45] // allows multiple users } }); console.log(i); If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array. import { sp } from \"@pnp/sp\"; const result = await sp.web.lists.getByTitle(\"UserFieldList\").items.getById(1).validateUpdateListItem([{ FieldName: \"UserField\", FieldValue: JSON.stringify([{ \"Key\": \"i:0#.f|membership|person@tenant.com\" }]), }, { FieldName: \"Title\", FieldValue: \"Test - Updated\", }]);","title":"User Fields"},{"location":"v2/sp/items/#lookup-fields","text":"What is said for User Fields is, in general, relevant to Lookup Fields: Lookup Field types: Single-valued lookup Multiple-valued lookup Id suffix should be appended to the end of lookups EntityPropertyName in payloads Numeric Ids for lookups' items should be passed as values import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import { getGUID } from \"@pnp/core\"; await sp.web.lists.getByTitle(\"LookupFields\").items.add({ Title: getGUID(), LookupFieldId: 2, // allows a single lookup value MultiLookupFieldId: { results: [ 1, 56 ] // allows multiple lookup value } });","title":"Lookup Fields"},{"location":"v2/sp/items/#add-multiple-items","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidadd\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); list.items.inBatch(batch).add({ Title: \"Batch 6\" }, entityTypeFullName).then(b => { console.log(b); }); list.items.inBatch(batch).add({ Title: \"Batch 7\" }, entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\");","title":"Add Multiple Items"},{"location":"v2/sp/items/#update","text":"The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const i = await list.items.getById(1).update({ Title: \"My New Title\", Description: \"Here is a new description\" }); console.log(i);","title":"Update"},{"location":"v2/sp/items/#getting-and-updating-a-collection-using-filter","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; // you are getting back a collection here const items: any[] = await sp.web.lists.getByTitle(\"MyList\").items.top(1).filter(\"Title eq 'A Title'\")(); // see if we got something if (items.length > 0) { const updatedItem = await sp.web.lists.getByTitle(\"MyList\").items.getById(items[0].Id).update({ Title: \"Updated Title\", }); console.log(JSON.stringify(updatedItem)); }","title":"Getting and updating a collection using filter"},{"location":"v2/sp/items/#update-multiple-items","text":"This approach avoids multiple calls for the same list's entity type name. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"rapidupdate\"); const entityTypeFullName = await list.getListItemEntityTypeFullName() let batch = sp.web.createBatch(); // note requirement of \"*\" eTag param - or use a specific eTag value as needed list.items.getById(1).inBatch(batch).update({ Title: \"Batch 6\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); list.items.getById(2).inBatch(batch).update({ Title: \"Batch 7\" }, \"*\", entityTypeFullName).then(b => { console.log(b); }); await batch.execute(); console.log(\"Done\")","title":"Update Multiple Items"},{"location":"v2/sp/items/#recycle","text":"To send an item to the recycle bin use recycle. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); const recycleBinIdentifier = await list.items.getById(1).recycle();","title":"Recycle"},{"location":"v2/sp/items/#delete","text":"Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).delete();","title":"Delete"},{"location":"v2/sp/items/#delete-with-params","text":"Added in 2.0.9 Deletes the item object with options. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; let list = sp.web.lists.getByTitle(\"MyList\"); await list.items.getById(1).deleteWithParams({ BypassSharedLock: true, }); The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true","title":"Delete With Params"},{"location":"v2/sp/items/#resolving-field-names","text":"It's a very common mistake trying wrong field names in the requests. Field's EntityPropertyName value should be used. The easiest way to get know EntityPropertyName is to use the following snippet: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/items\"; import \"@pnp/sp/fields\"; const response = await sp.web.lists .getByTitle('[Lists_Title]') .fields .select('Title, EntityPropertyName') .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`) (); console.log(response.map(field => { return { Title: field.Title, EntityPropertyName: field.EntityPropertyName }; })); Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.","title":"Resolving field names"},{"location":"v2/sp/items/#getparentinfos","text":"Added in 2.0.12 Gets information about an item, including details about the parent list, parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const item: any = await sp.web.lists.getByTitle(\"My List\").items.getById(1)(); await item.getParentInfos();","title":"getParentInfos"},{"location":"v2/sp/lists/","text":"@pnp/sp/lists \u00b6 Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet. ILists \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Lists, ILists } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; Preset: All import { sp, Lists, ILists } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Lists, ILists } from \"@pnp/sp/presets/core\"; Get List by Id \u00b6 Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the list by Id const list = sp.web.lists.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // we can use this 'list' variable to execute more queries on the list: const r = await list.select(\"Title\")(); // show the response from the server console.log(r.Title); Get List by Title \u00b6 You can also get a list from the collection by title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the default document library 'Documents' const list = sp.web.lists.getByTitle(\"Documents\"); // we can use this 'list' variable to run more queries on the list: const r = await list.select(\"Id\")(); // log the list Id to console console.log(r.Id); Add List \u00b6 You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide. // create a new list, passing only the title const listAddResult = await sp.web.lists.add(\"My new list\"); // we can work with the list created using the IListAddResult.list property: const r = await listAddResult.list.select(\"Title\")(); // log newly created list title to console console.log(r.Title); }); You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs. // this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings) const listAddResult = await sp.web.lists.add(\"My Doc Library\", \"This is a description of doc lib.\", 101, true, { OnQuickLaunch: true }); // get the Id of the newly added document library const r = await listAddResult.list.select(\"Id\")(); // log id to console console.log(r.Id); Ensure that a List exists (by title) \u00b6 Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings. // ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default): const listEnsureResult = await sp.web.lists.ensure(\"My List\"); // check if the list was created, or if it already existed: if (listEnsureResult.created) { console.log(\"My List was created!\"); } else { console.log(\"My List already existed!\"); } // work on the created/updated list const r = await listEnsureResult.list.select(\"Id\")(); // log the Id console.log(r.Id); If the list already exists, the other settings you provide will be used to update the existing list. // add a new list to the lists collection of the web sp.web.lists.add(\"My List 2\").then(async () => { // then call ensure on the created list with an updated description const listEnsureResult = await sp.web.lists.ensure(\"My List 2\", \"Updated description\"); // get the updated description const r = await listEnsureResult.list.select(\"Description\")(); // log the updated description console.log(r.Description); }); Ensure Site Assets Library exist \u00b6 Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages. // get Site Assets library const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title); Ensure Site Pages Library exist \u00b6 Gets a list that is the default location for wiki pages. // get Site Pages library const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title); IList \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { List, IList } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists\"; Preset: All import { sp, List, IList } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, List, IList } from \"@pnp/sp/presets/core\"; Update a list \u00b6 Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is \"*\") import { IListUpdateResult } from \"@pnp/sp/lists\"; // create a TypedHash object with the properties to update const updateProperties = { Description: \"This list title and description has been updated using PnPjs.\", Title: \"Updated title\", }; // update the list with the properties above list.update(updateProperties).then(async (l: IListUpdateResult) => { // get the updated title and description const r = await l.list.select(\"Title\", \"Description\")(); // log the updated properties to the console console.log(r.Title); console.log(r.Description); }); Get changes on a list \u00b6 From the change log, you can get a collection of changes that have occurred within the list based on the specified query. import { sp, IChangeQuery } from \"@pnp/sp\"; // build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore const changeQuery: IChangeQuery = { Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Rename: true, Restore: true, }; // get list changes const r = await list.getChanges(changeQuery); // log changes to console console.log(r); Get list items using a CAML Query \u00b6 You can get items from SharePoint using a CAML Query. import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml); // log resulting array to console console.log(r); If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment) import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml, \"RoleAssignments\"); // log resulting item array to console console.log(r); Get list items changes using a Token \u00b6 import { IChangeLogItemQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const changeLogItemQuery: IChangeLogItemQuery = { Contains: `Item16`, QueryOptions: ` FALSE False TRUE FALSE My List`, }; // get list items const r = await list.getListItemChangesSinceToken(changeLogItemQuery); // log resulting XML to console console.log(r); Recycle a list \u00b6 Removes the list from the web's list collection and puts it in the recycle bin. await list.recycle(); Render list data \u00b6 import { IRenderListData } from \"@pnp/sp/lists\"; // render list data, top 5 items const r: IRenderListData = await list.renderListData(\"5\"); // log array of items in response console.log(r.Row); Render list data as stream \u00b6 import { IRenderListDataParameters } from \"@pnp/sp/lists\"; // setup parameters object const renderListDataParams: IRenderListDataParameters = { ViewXml: \"5\", }; // render list data as stream const r = await list.renderListDataAsStream(renderListDataParams); // log array of items in response console.log(r.Row); Reserve list item Id for idempotent list item creation \u00b6 const listItemId = await list.reserveListItemId(); // log id to console console.log(listItemId); Get list item entity type name \u00b6 const entityTypeFullName = await list.getListItemEntityTypeFullName(); // log entity type name console.log(entityTypeFullName); Add a list item using path (folder), validation and set field values \u00b6 const list = await sp.webs.lists.getByTitle(\"MyList\").select(\"Title\", \"ParentWebUrl\")(); const formValues: IListItemFormUpdateValue[] = [ { FieldName: \"Title\", FieldValue: title, }, ]; list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`) content-types imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; contentTypes \u00b6 Get all content types for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.contentTypes(); fields imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; fields \u00b6 Get all the fields for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.fields(); Add a field to the site, then add the site field to a list const fld = await sp.site.rootWeb.fields.addText(\"MyField\"); await sp.web.lists.getByTitle(\"MyList\").fields.createFieldAsXml(fld.data.SchemaXml); folders imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; folders \u00b6 Get the root folder of a list. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.rootFolder(); forms imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/forms\"; Selective 2 import \"@pnp/sp/forms/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; forms \u00b6 const r = await list.forms(); items imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/items\"; Selective 2 import \"@pnp/sp/items/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; items \u00b6 Get a collection of list items. const r = await list.items(); views imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/views\"; Selective 2 import \"@pnp/sp/views/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; views \u00b6 Get the default view of the list const list = sp.web.lists.getByTitle(\"Documents\"); const views = await list.views(); const defaultView = await list.defaultView(); Get a list view by Id const view = await list.getView(defaultView.Id).select(\"Title\")(); security imports \u00b6 To work with list security, you can import the list methods as follows: import \"@pnp/sp/security/list\"; For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation. subscriptions imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/subscriptions\"; Selective 2 import \"@pnp/sp/subscriptions/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; subscriptions \u00b6 Get all subscriptions on the list const list = sp.web.lists.getByTitle(\"Documents\"); const subscriptions = await list.subscriptions(); user-custom-actions imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; userCustomActions \u00b6 Get a collection of the list's user custom actions. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.userCustomActions(); getParentInfos \u00b6 Added in 2.0.12 Gets information about an list, including details about the parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const list = sp.web.lists.getByTitle(\"Documents\"); await list.getParentInfos();","title":"@pnp/sp/lists"},{"location":"v2/sp/lists/#pnpsplists","text":"Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.","title":"@pnp/sp/lists"},{"location":"v2/sp/lists/#ilists","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; import { Lists, ILists } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; Preset: All import { sp, Lists, ILists } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Lists, ILists } from \"@pnp/sp/presets/core\";","title":"ILists"},{"location":"v2/sp/lists/#get-list-by-id","text":"Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the list by Id const list = sp.web.lists.getById(\"03b05ff4-d95d-45ed-841d-3855f77a2483\"); // we can use this 'list' variable to execute more queries on the list: const r = await list.select(\"Title\")(); // show the response from the server console.log(r.Title);","title":"Get List by Id"},{"location":"v2/sp/lists/#get-list-by-title","text":"You can also get a list from the collection by title. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; // get the default document library 'Documents' const list = sp.web.lists.getByTitle(\"Documents\"); // we can use this 'list' variable to run more queries on the list: const r = await list.select(\"Id\")(); // log the list Id to console console.log(r.Id);","title":"Get List by Title"},{"location":"v2/sp/lists/#add-list","text":"You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide. // create a new list, passing only the title const listAddResult = await sp.web.lists.add(\"My new list\"); // we can work with the list created using the IListAddResult.list property: const r = await listAddResult.list.select(\"Title\")(); // log newly created list title to console console.log(r.Title); }); You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs. // this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings) const listAddResult = await sp.web.lists.add(\"My Doc Library\", \"This is a description of doc lib.\", 101, true, { OnQuickLaunch: true }); // get the Id of the newly added document library const r = await listAddResult.list.select(\"Id\")(); // log id to console console.log(r.Id);","title":"Add List"},{"location":"v2/sp/lists/#ensure-that-a-list-exists-by-title","text":"Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings. // ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default): const listEnsureResult = await sp.web.lists.ensure(\"My List\"); // check if the list was created, or if it already existed: if (listEnsureResult.created) { console.log(\"My List was created!\"); } else { console.log(\"My List already existed!\"); } // work on the created/updated list const r = await listEnsureResult.list.select(\"Id\")(); // log the Id console.log(r.Id); If the list already exists, the other settings you provide will be used to update the existing list. // add a new list to the lists collection of the web sp.web.lists.add(\"My List 2\").then(async () => { // then call ensure on the created list with an updated description const listEnsureResult = await sp.web.lists.ensure(\"My List 2\", \"Updated description\"); // get the updated description const r = await listEnsureResult.list.select(\"Description\")(); // log the updated description console.log(r.Description); });","title":"Ensure that a List exists (by title)"},{"location":"v2/sp/lists/#ensure-site-assets-library-exist","text":"Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages. // get Site Assets library const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title);","title":"Ensure Site Assets Library exist"},{"location":"v2/sp/lists/#ensure-site-pages-library-exist","text":"Gets a list that is the default location for wiki pages. // get Site Pages library const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary(); // get the Title const r = await siteAssetsList.select(\"Title\")(); // log Title console.log(r.Title);","title":"Ensure Site Pages Library exist"},{"location":"v2/sp/lists/#ilist","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { List, IList } from \"@pnp/sp/lists\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/lists\"; Preset: All import { sp, List, IList } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, List, IList } from \"@pnp/sp/presets/core\";","title":"IList"},{"location":"v2/sp/lists/#update-a-list","text":"Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is \"*\") import { IListUpdateResult } from \"@pnp/sp/lists\"; // create a TypedHash object with the properties to update const updateProperties = { Description: \"This list title and description has been updated using PnPjs.\", Title: \"Updated title\", }; // update the list with the properties above list.update(updateProperties).then(async (l: IListUpdateResult) => { // get the updated title and description const r = await l.list.select(\"Title\", \"Description\")(); // log the updated properties to the console console.log(r.Title); console.log(r.Description); });","title":"Update a list"},{"location":"v2/sp/lists/#get-changes-on-a-list","text":"From the change log, you can get a collection of changes that have occurred within the list based on the specified query. import { sp, IChangeQuery } from \"@pnp/sp\"; // build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore const changeQuery: IChangeQuery = { Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Rename: true, Restore: true, }; // get list changes const r = await list.getChanges(changeQuery); // log changes to console console.log(r);","title":"Get changes on a list"},{"location":"v2/sp/lists/#get-list-items-using-a-caml-query","text":"You can get items from SharePoint using a CAML Query. import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml); // log resulting array to console console.log(r); If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment) import { ICamlQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const caml: ICamlQuery = { ViewXml: \"5\", }; // get list items const r = await list.getItemsByCAMLQuery(caml, \"RoleAssignments\"); // log resulting item array to console console.log(r);","title":"Get list items using a CAML Query"},{"location":"v2/sp/lists/#get-list-items-changes-using-a-token","text":"import { IChangeLogItemQuery } from \"@pnp/sp/lists\"; // build the caml query object (in this example, we include Title field and limit rows to 5) const changeLogItemQuery: IChangeLogItemQuery = { Contains: `Item16`, QueryOptions: ` FALSE False TRUE FALSE My List`, }; // get list items const r = await list.getListItemChangesSinceToken(changeLogItemQuery); // log resulting XML to console console.log(r);","title":"Get list items changes using a Token"},{"location":"v2/sp/lists/#recycle-a-list","text":"Removes the list from the web's list collection and puts it in the recycle bin. await list.recycle();","title":"Recycle a list"},{"location":"v2/sp/lists/#render-list-data","text":"import { IRenderListData } from \"@pnp/sp/lists\"; // render list data, top 5 items const r: IRenderListData = await list.renderListData(\"5\"); // log array of items in response console.log(r.Row);","title":"Render list data"},{"location":"v2/sp/lists/#render-list-data-as-stream","text":"import { IRenderListDataParameters } from \"@pnp/sp/lists\"; // setup parameters object const renderListDataParams: IRenderListDataParameters = { ViewXml: \"5\", }; // render list data as stream const r = await list.renderListDataAsStream(renderListDataParams); // log array of items in response console.log(r.Row);","title":"Render list data as stream"},{"location":"v2/sp/lists/#reserve-list-item-id-for-idempotent-list-item-creation","text":"const listItemId = await list.reserveListItemId(); // log id to console console.log(listItemId);","title":"Reserve list item Id for idempotent list item creation"},{"location":"v2/sp/lists/#get-list-item-entity-type-name","text":"const entityTypeFullName = await list.getListItemEntityTypeFullName(); // log entity type name console.log(entityTypeFullName);","title":"Get list item entity type name"},{"location":"v2/sp/lists/#add-a-list-item-using-path-folder-validation-and-set-field-values","text":"const list = await sp.webs.lists.getByTitle(\"MyList\").select(\"Title\", \"ParentWebUrl\")(); const formValues: IListItemFormUpdateValue[] = [ { FieldName: \"Title\", FieldValue: title, }, ]; list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)","title":"Add a list item using path (folder), validation and set field values"},{"location":"v2/sp/lists/#content-types-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"content-types imports"},{"location":"v2/sp/lists/#contenttypes","text":"Get all content types for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.contentTypes();","title":"contentTypes"},{"location":"v2/sp/lists/#fields-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"fields imports"},{"location":"v2/sp/lists/#fields","text":"Get all the fields for a list const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.fields(); Add a field to the site, then add the site field to a list const fld = await sp.site.rootWeb.fields.addText(\"MyField\"); await sp.web.lists.getByTitle(\"MyList\").fields.createFieldAsXml(fld.data.SchemaXml);","title":"fields"},{"location":"v2/sp/lists/#folders-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"folders imports"},{"location":"v2/sp/lists/#folders","text":"Get the root folder of a list. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.rootFolder();","title":"folders"},{"location":"v2/sp/lists/#forms-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/forms\"; Selective 2 import \"@pnp/sp/forms/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"forms imports"},{"location":"v2/sp/lists/#forms","text":"const r = await list.forms();","title":"forms"},{"location":"v2/sp/lists/#items-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/items\"; Selective 2 import \"@pnp/sp/items/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"items imports"},{"location":"v2/sp/lists/#items","text":"Get a collection of list items. const r = await list.items();","title":"items"},{"location":"v2/sp/lists/#views-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/views\"; Selective 2 import \"@pnp/sp/views/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"views imports"},{"location":"v2/sp/lists/#views","text":"Get the default view of the list const list = sp.web.lists.getByTitle(\"Documents\"); const views = await list.views(); const defaultView = await list.defaultView(); Get a list view by Id const view = await list.getView(defaultView.Id).select(\"Title\")();","title":"views"},{"location":"v2/sp/lists/#security-imports","text":"To work with list security, you can import the list methods as follows: import \"@pnp/sp/security/list\"; For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.","title":"security imports"},{"location":"v2/sp/lists/#subscriptions-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/subscriptions\"; Selective 2 import \"@pnp/sp/subscriptions/list\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"subscriptions imports"},{"location":"v2/sp/lists/#subscriptions","text":"Get all subscriptions on the list const list = sp.web.lists.getByTitle(\"Documents\"); const subscriptions = await list.subscriptions();","title":"subscriptions"},{"location":"v2/sp/lists/#user-custom-actions-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"user-custom-actions imports"},{"location":"v2/sp/lists/#usercustomactions","text":"Get a collection of the list's user custom actions. const list = sp.web.lists.getByTitle(\"Documents\"); const r = await list.userCustomActions();","title":"userCustomActions"},{"location":"v2/sp/lists/#getparentinfos","text":"Added in 2.0.12 Gets information about an list, including details about the parent list root folder, and parent web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/items\"; const list = sp.web.lists.getByTitle(\"Documents\"); await list.getParentInfos();","title":"getParentInfos"},{"location":"v2/sp/navigation/","text":"@pnp/sp - navigation \u00b6 Navigation Service \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; getMenuState \u00b6 The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\\,containingcomma NOTE: the , separator can be escaped using the \\ as escape character as done in the example above. The string above would split like: property1 property2 property3,containingcomma import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; // Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. const state = await sp.navigation.getMenuState(); // Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 const state2 = await sp.navigation.getMenuState(\"1002\", 5); // Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 const state3 = await sp.navigation.getMenuState(null, 5, \"CurrentNavSiteMapProviderNoEncode\"); getMenuNodeKey \u00b6 Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; const key = await sp.navigation.getMenuNodeKey(\"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\"); Web Navigation \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; The navigation object contains two properties \"quicklaunch\" and \"topnavigationbar\". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar. Get navigation \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const top = await sp.web.navigation.topNavigationBar(); const quick = await sp.web.navigation.quicklaunch(); For the following examples we will refer to a variable named \"nav\" that is understood to be one of topNavigationBar or quicklaunch. getById \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node = await nav.getById(3)(); add \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const result = await nav.add(\"Node Title\", \"/sites/dev/pages/mypage.aspx\", true); const nodeDataRaw = result.data; // request the data from the created node const nodeData = result.node(); moveAfter \u00b6 Places a navigation node after another node in the tree import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true); const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true); const node1 = await node1result.node(); const node2 = await node2result.node(); await nav.moveAfter(node1.Id, node2.Id); Delete \u00b6 Deletes a given node import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true); let nodes = await nav(); // check we added a node let index = nodes.findIndex(n => n.Id === node1result.data.Id) // index >= 0 // delete a node await nav.getById(node1result.data.Id).delete(); nodes = await nav(); index = nodes.findIndex(n => n.Id === node1result.data.Id) // index = -1 Update \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; await nav.getById(4).update({ Title: \"A new title\", }); Children \u00b6 The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const childrenData = await nav.getById(1).children(); // add a child await nav.getById(1).children.add(\"Title\", \"Url\", true);","title":"@pnp/sp - navigation"},{"location":"v2/sp/navigation/#pnpsp-navigation","text":"","title":"@pnp/sp - navigation"},{"location":"v2/sp/navigation/#navigation-service","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\";","title":"Navigation Service"},{"location":"v2/sp/navigation/#getmenustate","text":"The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy. The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\\,containingcomma NOTE: the , separator can be escaped using the \\ as escape character as done in the example above. The string above would split like: property1 property2 property3,containingcomma import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; // Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels. const state = await sp.navigation.getMenuState(); // Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5 const state2 = await sp.navigation.getMenuState(\"1002\", 5); // Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5 const state3 = await sp.navigation.getMenuState(null, 5, \"CurrentNavSiteMapProviderNoEncode\");","title":"getMenuState"},{"location":"v2/sp/navigation/#getmenunodekey","text":"Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/navigation\"; const key = await sp.navigation.getMenuNodeKey(\"/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx\");","title":"getMenuNodeKey"},{"location":"v2/sp/navigation/#web-navigation","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; The navigation object contains two properties \"quicklaunch\" and \"topnavigationbar\". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.","title":"Web Navigation"},{"location":"v2/sp/navigation/#get-navigation","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const top = await sp.web.navigation.topNavigationBar(); const quick = await sp.web.navigation.quicklaunch(); For the following examples we will refer to a variable named \"nav\" that is understood to be one of topNavigationBar or quicklaunch.","title":"Get navigation"},{"location":"v2/sp/navigation/#getbyid","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node = await nav.getById(3)();","title":"getById"},{"location":"v2/sp/navigation/#add","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const result = await nav.add(\"Node Title\", \"/sites/dev/pages/mypage.aspx\", true); const nodeDataRaw = result.data; // request the data from the created node const nodeData = result.node();","title":"add"},{"location":"v2/sp/navigation/#moveafter","text":"Places a navigation node after another node in the tree import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true); const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true); const node1 = await node1result.node(); const node2 = await node2result.node(); await nav.moveAfter(node1.Id, node2.Id);","title":"moveAfter"},{"location":"v2/sp/navigation/#delete","text":"Deletes a given node import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true); let nodes = await nav(); // check we added a node let index = nodes.findIndex(n => n.Id === node1result.data.Id) // index >= 0 // delete a node await nav.getById(node1result.data.Id).delete(); nodes = await nav(); index = nodes.findIndex(n => n.Id === node1result.data.Id) // index = -1","title":"Delete"},{"location":"v2/sp/navigation/#update","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; await nav.getById(4).update({ Title: \"A new title\", });","title":"Update"},{"location":"v2/sp/navigation/#children","text":"The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/navigation\"; const childrenData = await nav.getById(1).children(); // add a child await nav.getById(1).children.add(\"Title\", \"Url\", true);","title":"Children"},{"location":"v2/sp/permissions/","text":"@pnp/sp - permissions \u00b6 A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables. Get Role Assignments \u00b6 This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const roles = await sp.web.roleAssignments(); Logger.writeJSON(roles); First Unique Ancestor Securable Object \u00b6 This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const obj = await sp.web.firstUniqueAncestorSecurableObject(); Logger.writeJSON(obj); User Effective Permissions \u00b6 This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const perms = await sp.web.getUserEffectivePermissions(\"i:0#.f|membership|user@site.com\"); Logger.writeJSON(perms); const perms2 = await sp.web.getCurrentUserEffectivePermissions(); Logger.writeJSON(perms2); User Has Permissions \u00b6 Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.userHasPermissions(\"i:0#.f|membership|user@site.com\", PermissionKind.ApproveItems); console.log(perms); const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems); console.log(perms2); Has Permissions \u00b6 If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.getCurrentUserEffectivePermissions(); if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) { // ... }","title":"@pnp/sp - permissions"},{"location":"v2/sp/permissions/#pnpsp-permissions","text":"A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user. Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.","title":"@pnp/sp - permissions"},{"location":"v2/sp/permissions/#get-role-assignments","text":"This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const roles = await sp.web.roleAssignments(); Logger.writeJSON(roles);","title":"Get Role Assignments"},{"location":"v2/sp/permissions/#first-unique-ancestor-securable-object","text":"This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const obj = await sp.web.firstUniqueAncestorSecurableObject(); Logger.writeJSON(obj);","title":"First Unique Ancestor Securable Object"},{"location":"v2/sp/permissions/#user-effective-permissions","text":"This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/security\"; import { Logger } from \"@pnp/logging\"; const perms = await sp.web.getUserEffectivePermissions(\"i:0#.f|membership|user@site.com\"); Logger.writeJSON(perms); const perms2 = await sp.web.getCurrentUserEffectivePermissions(); Logger.writeJSON(perms2);","title":"User Effective Permissions"},{"location":"v2/sp/permissions/#user-has-permissions","text":"Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.userHasPermissions(\"i:0#.f|membership|user@site.com\", PermissionKind.ApproveItems); console.log(perms); const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems); console.log(perms2);","title":"User Has Permissions"},{"location":"v2/sp/permissions/#has-permissions","text":"If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below. import { sp } from \"@pnp/sp\"; import { PermissionKind } from \"@pnp/sp/security\"; const perms = await sp.web.getCurrentUserEffectivePermissions(); if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) { // ... }","title":"Has Permissions"},{"location":"v2/sp/profiles/","text":"@pnp/sp/profiles \u00b6 The profile services allows you to work with the SharePoint User Profile Store. Profiles \u00b6 Profiles is accessed directly from the root sp object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/profiles\"; Get edit profile link for the current user \u00b6 editProfileLink(): Promise const editProfileLink = await sp.profiles.editProfileLink(); console.log(\"My edit profile link =\" + editProfileLink); Is My People List Public \u00b6 Provides a boolean that indicates if the current users \"People I'm Following\" list is public or not isMyPeopleListPublic(): Promise const isPublic = await sp.profiles.isMyPeopleListPublic(); console.log(\"Is my Following list Public =\" + isPubic); Find out if the current user is followed by another user \u00b6 Provides a boolean that indicates if the current users is followed by a specific user. amIFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const isFollowedBy = await sp.profiles.amIFollowedBy(loginName); console.log(\"Is \" + loginName + \" following me? \" + isFollowedBy); Find out if I am following a specific user \u00b6 Provides a boolean that indicates if the current users is followed by a specific user. amIFollowing(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const following = await sp.profiles.amIFollowing(loginName); console.log(\"Am I following \" + loginName + \"? \" + following); Get the tags I follow \u00b6 Gets the tags the current user is following. Accepts max count, default is 20. getFollowedTags(maxCount = 20): Promise const tags = await sp.profiles.getFollowedTags(); console.log(tags); Get followers for a specific user \u00b6 Gets the people who are following the specified user. getFollowersFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); }); Get followers for the current \u00b6 Gets the people who are following the current user. myFollowers(): ISharePointQueryableCollection const folowers = await sp.profiles.myFollowers(); console.log(folowers); Get the properties for the current user \u00b6 Gets user properties for the current user. myProperties(): _SharePointQueryableInstance const profile = await sp.profiles.myProperties(); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName); Gets people specified user is following \u00b6 getPeopleFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const folowers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); }); Gets properties for a specified user \u00b6 getPropertiesFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const profile = await sp.profiles.getPropertiesFor(loginName); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in inconvenient Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName); Gets most popular tags \u00b6 Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first trendingTags(): Promise const tags = await sp.profiles.trendingTags(); tags.Items.forEach((tag) => { console.log(tag); }); Gets specified user profile property for the specified user \u00b6 getUserProfilePropertyFor(loginName: string, propertyName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"AccountName\"; const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName); console.log(property); Hide specific user from list of suggested people \u00b6 Removes the specified user from the user's list of suggested people to follow. hideSuggestion(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.hideSuggestion(loginName); Is one user following another \u00b6 Indicates whether the first user is following the second user. First parameter is the account name of the user who might be following the followee. Second parameter is the account name of the user who might be followed by the follower. isFollowing(follower: string, followee: string): Promise const follower = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followee = \"i:0#.f|membership|testuser2@mytenant.onmicrosoft.com\"; const isFollowing = await sp.profiles.isFollowing(follower, followee); console.log(isFollowing); Set User Profile Picture \u00b6 Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB. setMyProfilePic(profilePicSource: Blob): Promise import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/profiles\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files\"; // get the blob object through a request or from a file input const blob = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.getByName(\"profile.jpg\").getBlob(); await sp.profiles.setMyProfilePic(blob); Sets single value User Profile property \u00b6 accountName The account name of the user propertyName Property name propertyValue Property value setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.setSingleValueProfileProperty(loginName, \"CellPhone\", \"(123) 555-1212\"); Sets a mult-value User Profile property \u00b6 accountName The account name of the user propertyName Property name propertyValues Property values setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"SPS-Skills\"; const propertyValues = [\"SharePoint\", \"Office 365\", \"Architecture\", \"Azure\"]; await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues); const profile = await sp.profiles.getPropertiesFor(loginName); var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(profile.userProperties[propertyName]); Create Personal Site for specified users \u00b6 Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Emails The email addresses of the users to provision sites for createPersonalSiteEnqueueBulk(...emails: string[]): Promise let userEmails: string[] = [\"testuser1@mytenant.onmicrosoft.com\", \"testuser2@mytenant.onmicrosoft.com\"]; await sp.profiles.createPersonalSiteEnqueueBulk(userEmails); Get the user profile of the owner for the current site \u00b6 ownerUserProfile(): Promise const profile = await sp.profiles.ownerUserProfile(); console.log(profile); Get the user profile of the current user \u00b6 userProfile(): Promise const profile = await sp.profiles.userProfile(); console.log(profile); Create personal site for current user \u00b6 createPersonalSite(interactiveRequest = false): Promise await sp.profiles.createPersonalSite(); Make all profile data public or private \u00b6 Set the privacy settings for all social data. shareAllSocialData(share: boolean): Promise await sp.profiles.shareAllSocialData(true); Resolve a user or group \u00b6 Resolves user or group using specified query parameters clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result); Search a user or group \u00b6 Searches for users or groups using specified query parameters clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result);","title":"@pnp/sp/profiles"},{"location":"v2/sp/profiles/#pnpspprofiles","text":"The profile services allows you to work with the SharePoint User Profile Store.","title":"@pnp/sp/profiles"},{"location":"v2/sp/profiles/#profiles","text":"Profiles is accessed directly from the root sp object. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/profiles\";","title":"Profiles"},{"location":"v2/sp/profiles/#get-edit-profile-link-for-the-current-user","text":"editProfileLink(): Promise const editProfileLink = await sp.profiles.editProfileLink(); console.log(\"My edit profile link =\" + editProfileLink);","title":"Get edit profile link for the current user"},{"location":"v2/sp/profiles/#is-my-people-list-public","text":"Provides a boolean that indicates if the current users \"People I'm Following\" list is public or not isMyPeopleListPublic(): Promise const isPublic = await sp.profiles.isMyPeopleListPublic(); console.log(\"Is my Following list Public =\" + isPubic);","title":"Is My People List Public"},{"location":"v2/sp/profiles/#find-out-if-the-current-user-is-followed-by-another-user","text":"Provides a boolean that indicates if the current users is followed by a specific user. amIFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const isFollowedBy = await sp.profiles.amIFollowedBy(loginName); console.log(\"Is \" + loginName + \" following me? \" + isFollowedBy);","title":"Find out if the current user is followed by another user"},{"location":"v2/sp/profiles/#find-out-if-i-am-following-a-specific-user","text":"Provides a boolean that indicates if the current users is followed by a specific user. amIFollowing(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const following = await sp.profiles.amIFollowing(loginName); console.log(\"Am I following \" + loginName + \"? \" + following);","title":"Find out if I am following a specific user"},{"location":"v2/sp/profiles/#get-the-tags-i-follow","text":"Gets the tags the current user is following. Accepts max count, default is 20. getFollowedTags(maxCount = 20): Promise const tags = await sp.profiles.getFollowedTags(); console.log(tags);","title":"Get the tags I follow"},{"location":"v2/sp/profiles/#get-followers-for-a-specific-user","text":"Gets the people who are following the specified user. getFollowersFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); });","title":"Get followers for a specific user"},{"location":"v2/sp/profiles/#get-followers-for-the-current","text":"Gets the people who are following the current user. myFollowers(): ISharePointQueryableCollection const folowers = await sp.profiles.myFollowers(); console.log(folowers);","title":"Get followers for the current"},{"location":"v2/sp/profiles/#get-the-properties-for-the-current-user","text":"Gets user properties for the current user. myProperties(): _SharePointQueryableInstance const profile = await sp.profiles.myProperties(); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName);","title":"Get the properties for the current user"},{"location":"v2/sp/profiles/#gets-people-specified-user-is-following","text":"getPeopleFollowedBy(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const folowers = await sp.profiles.getFollowersFor(loginName); followers.forEach((value) => { console.log(value); });","title":"Gets people specified user is following"},{"location":"v2/sp/profiles/#gets-properties-for-a-specified-user","text":"getPropertiesFor(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const profile = await sp.profiles.getPropertiesFor(loginName); console.log(profile.DisplayName); console.log(profile.Email); console.log(profile.Title); console.log(profile.UserProfileProperties.length); // Properties are stored in inconvenient Key/Value pairs, // so parse into an object called userProperties var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(\"Account Name: \" + profile.userProperties.AccountName);","title":"Gets properties for a specified user"},{"location":"v2/sp/profiles/#gets-most-popular-tags","text":"Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first trendingTags(): Promise const tags = await sp.profiles.trendingTags(); tags.Items.forEach((tag) => { console.log(tag); });","title":"Gets most popular tags"},{"location":"v2/sp/profiles/#gets-specified-user-profile-property-for-the-specified-user","text":"getUserProfilePropertyFor(loginName: string, propertyName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"AccountName\"; const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName); console.log(property);","title":"Gets specified user profile property for the specified user"},{"location":"v2/sp/profiles/#hide-specific-user-from-list-of-suggested-people","text":"Removes the specified user from the user's list of suggested people to follow. hideSuggestion(loginName: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.hideSuggestion(loginName);","title":"Hide specific user from list of suggested people"},{"location":"v2/sp/profiles/#is-one-user-following-another","text":"Indicates whether the first user is following the second user. First parameter is the account name of the user who might be following the followee. Second parameter is the account name of the user who might be followed by the follower. isFollowing(follower: string, followee: string): Promise const follower = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const followee = \"i:0#.f|membership|testuser2@mytenant.onmicrosoft.com\"; const isFollowing = await sp.profiles.isFollowing(follower, followee); console.log(isFollowing);","title":"Is one user following another"},{"location":"v2/sp/profiles/#set-user-profile-picture","text":"Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB. setMyProfilePic(profilePicSource: Blob): Promise import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/profiles\"; import \"@pnp/sp/folders\"; import \"@pnp/sp/files\"; // get the blob object through a request or from a file input const blob = await sp.web.lists.getByTitle(\"Documents\").rootFolder.files.getByName(\"profile.jpg\").getBlob(); await sp.profiles.setMyProfilePic(blob);","title":"Set User Profile Picture"},{"location":"v2/sp/profiles/#sets-single-value-user-profile-property","text":"accountName The account name of the user propertyName Property name propertyValue Property value setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; await sp.profiles.setSingleValueProfileProperty(loginName, \"CellPhone\", \"(123) 555-1212\");","title":"Sets single value User Profile property"},{"location":"v2/sp/profiles/#sets-a-mult-value-user-profile-property","text":"accountName The account name of the user propertyName Property name propertyValues Property values setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise const loginName = \"i:0#.f|membership|testuser@mytenant.onmicrosoft.com\"; const propertyName = \"SPS-Skills\"; const propertyValues = [\"SharePoint\", \"Office 365\", \"Architecture\", \"Azure\"]; await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues); const profile = await sp.profiles.getPropertiesFor(loginName); var props = {}; profile.UserProfileProperties.forEach((prop) => { props[prop.Key] = prop.Value; }); profile.userProperties = props; console.log(profile.userProperties[propertyName]);","title":"Sets a mult-value User Profile property"},{"location":"v2/sp/profiles/#create-personal-site-for-specified-users","text":"Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) Emails The email addresses of the users to provision sites for createPersonalSiteEnqueueBulk(...emails: string[]): Promise let userEmails: string[] = [\"testuser1@mytenant.onmicrosoft.com\", \"testuser2@mytenant.onmicrosoft.com\"]; await sp.profiles.createPersonalSiteEnqueueBulk(userEmails);","title":"Create Personal Site for specified users"},{"location":"v2/sp/profiles/#get-the-user-profile-of-the-owner-for-the-current-site","text":"ownerUserProfile(): Promise const profile = await sp.profiles.ownerUserProfile(); console.log(profile);","title":"Get the user profile of the owner for the current site"},{"location":"v2/sp/profiles/#get-the-user-profile-of-the-current-user","text":"userProfile(): Promise const profile = await sp.profiles.userProfile(); console.log(profile);","title":"Get the user profile of the current user"},{"location":"v2/sp/profiles/#create-personal-site-for-current-user","text":"createPersonalSite(interactiveRequest = false): Promise await sp.profiles.createPersonalSite();","title":"Create personal site for current user"},{"location":"v2/sp/profiles/#make-all-profile-data-public-or-private","text":"Set the privacy settings for all social data. shareAllSocialData(share: boolean): Promise await sp.profiles.shareAllSocialData(true);","title":"Make all profile data public or private"},{"location":"v2/sp/profiles/#resolve-a-user-or-group","text":"Resolves user or group using specified query parameters clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result);","title":"Resolve a user or group"},{"location":"v2/sp/profiles/#search-a-user-or-group","text":"Searches for users or groups using specified query parameters clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise const result = await sp.profiles.clientPeoplePickerSearchUser({ AllowEmailAddresses: true, AllowMultipleEntities: false, MaximumEntitySuggestions: 25, QueryString: 'John' }); console.log(result);","title":"Search a user or group"},{"location":"v2/sp/regional-settings/","text":"@pnp/sp/regional-settings \u00b6 The regional settings module helps with managing dates and times across various timezones. IRegionalSettings \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IRegionalSettings, ITimeZone, ITimeZones, RegionalSettings, TimeZone, TimeZones, } from \"@pnp/sp/regional-settings\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get all the web's regional settings const s = await sp.web.regionalSettings(); // select only some settings to return const s2 = await sp.web.regionalSettings.select(\"DecimalSeparator\", \"ListSeparator\", \"IsUIRightToLeft\")(); Installed Languages \u00b6 You can get a list of the installed languages in the web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; const s = await sp.web.regionalSettings.getInstalledLanguages(); The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions. TimeZones \u00b6 You can also get information about the selected timezone in the web and all of the defined timezones. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get the web's configured timezone const s = await sp.web.regionalSettings.timeZone(); // select just the Description and Id const s2 = await sp.web.regionalSettings.timeZone.select(\"Description\", \"Id\")(); // get all the timezones const s3 = await sp.web.regionalSettings.timeZones(); // get a specific timezone by id // list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx const s4 = await sp.web.regionalSettings.timeZones.getById(23); const s5 = await s.localTimeToUTC(new Date()); // convert a given date from web's local time to UTC time const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date()); // convert a given date from UTC time to web's local time const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date()) const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0)) Title and Description Resources \u00b6 Added in 2.0.4 Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; // // The below methods appears on // - Web // - List // - Field // - ContentType // - User Custom Action // // after you import @pnp/sp/regional-settings // // you can also import just parts of the regional settings: // import \"@pnp/sp/regional-settings/web\"; // import \"@pnp/sp/regional-settings/list\"; // import \"@pnp/sp/regional-settings/content-type\"; // import \"@pnp/sp/regional-settings/field\"; // import \"@pnp/sp/regional-settings/user-custom-actions\"; const title = await sp.web.titleResource(\"en-us\"); const title2 = await sp.web.titleResource(\"de-de\"); const description = await sp.web.descriptionResource(\"en-us\"); const description2 = await sp.web.descriptionResource(\"de-de\"); You can only read the values through the REST API, not set the value.","title":"@pnp/sp/regional-settings"},{"location":"v2/sp/regional-settings/#pnpspregional-settings","text":"The regional settings module helps with managing dates and times across various timezones.","title":"@pnp/sp/regional-settings"},{"location":"v2/sp/regional-settings/#iregionalsettings","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import { IRegionalSettings, ITimeZone, ITimeZones, RegionalSettings, TimeZone, TimeZones, } from \"@pnp/sp/regional-settings\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get all the web's regional settings const s = await sp.web.regionalSettings(); // select only some settings to return const s2 = await sp.web.regionalSettings.select(\"DecimalSeparator\", \"ListSeparator\", \"IsUIRightToLeft\")();","title":"IRegionalSettings"},{"location":"v2/sp/regional-settings/#installed-languages","text":"You can get a list of the installed languages in the web. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; const s = await sp.web.regionalSettings.getInstalledLanguages(); The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.","title":"Installed Languages"},{"location":"v2/sp/regional-settings/#timezones","text":"You can also get information about the selected timezone in the web and all of the defined timezones. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings/web\"; // get the web's configured timezone const s = await sp.web.regionalSettings.timeZone(); // select just the Description and Id const s2 = await sp.web.regionalSettings.timeZone.select(\"Description\", \"Id\")(); // get all the timezones const s3 = await sp.web.regionalSettings.timeZones(); // get a specific timezone by id // list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx const s4 = await sp.web.regionalSettings.timeZones.getById(23); const s5 = await s.localTimeToUTC(new Date()); // convert a given date from web's local time to UTC time const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date()); // convert a given date from UTC time to web's local time const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date()) const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))","title":"TimeZones"},{"location":"v2/sp/regional-settings/#title-and-description-resources","text":"Added in 2.0.4 Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/regional-settings\"; // // The below methods appears on // - Web // - List // - Field // - ContentType // - User Custom Action // // after you import @pnp/sp/regional-settings // // you can also import just parts of the regional settings: // import \"@pnp/sp/regional-settings/web\"; // import \"@pnp/sp/regional-settings/list\"; // import \"@pnp/sp/regional-settings/content-type\"; // import \"@pnp/sp/regional-settings/field\"; // import \"@pnp/sp/regional-settings/user-custom-actions\"; const title = await sp.web.titleResource(\"en-us\"); const title2 = await sp.web.titleResource(\"de-de\"); const description = await sp.web.descriptionResource(\"en-us\"); const description2 = await sp.web.descriptionResource(\"de-de\"); You can only read the values through the REST API, not set the value.","title":"Title and Description Resources"},{"location":"v2/sp/related-items/","text":"@pnp/sp/related-items \u00b6 The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection. Setup \u00b6 Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work. import { sp, extractWebUrl } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/related-items/web\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import \"@pnp/sp/files/list\"; import { IList } from \"@pnp/sp/lists\"; import { getRandomString } from \"@pnp/core\"; // setup some lists (or just use existing ones this is just to show the complete process) // we need two lists to use for creating related items, they need to use template 107 (task list) const ler1 = await sp.web.lists.ensure(\"RelatedItemsSourceList\", \"\", 107); const ler2 = await sp.web.lists.ensure(\"RelatedItemsTargetList\", \"\", 107); const sourceList = ler1.list; const targetList = ler2.list; const sourceListName = await sourceList.select(\"Id\")().then(r => r.Id); const targetListName = await targetList.select(\"Id\")().then(r => r.Id); // or whatever you need to get the web url, both our example lists are in the same web. const webUrl = sp.web.toUrl(); // ...individual samples start here addSingleLink \u00b6 const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); addSingleLinkToUrl \u00b6 This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document. // get a file's server relative url in some manner, here we add one const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, \"Content\", true).then(r => r.data); // add an item or get an item from the task list const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl); addSingleLinkFromUrl \u00b6 This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method. deleteSingleLink \u00b6 This method allows you to delete a link previously created. const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add the link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); // delete the link await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); getRelatedItems \u00b6 Gets the related items for an item import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id); // items.length === 2 Related items are defined by the IRelatedItem interface export interface IRelatedItem { ListId: string; ItemId: number; Url: string; Title: string; WebId: string; IconUrl: string; } getPageOneRelatedItems \u00b6 Gets an abbreviated set of related items import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id); // items.length === 2","title":"@pnp/sp/related-items"},{"location":"v2/sp/related-items/#pnpsprelated-items","text":"The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.","title":"@pnp/sp/related-items"},{"location":"v2/sp/related-items/#setup","text":"Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work. import { sp, extractWebUrl } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/related-items/web\"; import \"@pnp/sp/lists/web\"; import \"@pnp/sp/items/list\"; import \"@pnp/sp/files/list\"; import { IList } from \"@pnp/sp/lists\"; import { getRandomString } from \"@pnp/core\"; // setup some lists (or just use existing ones this is just to show the complete process) // we need two lists to use for creating related items, they need to use template 107 (task list) const ler1 = await sp.web.lists.ensure(\"RelatedItemsSourceList\", \"\", 107); const ler2 = await sp.web.lists.ensure(\"RelatedItemsTargetList\", \"\", 107); const sourceList = ler1.list; const targetList = ler2.list; const sourceListName = await sourceList.select(\"Id\")().then(r => r.Id); const targetListName = await targetList.select(\"Id\")().then(r => r.Id); // or whatever you need to get the web url, both our example lists are in the same web. const webUrl = sp.web.toUrl(); // ...individual samples start here","title":"Setup"},{"location":"v2/sp/related-items/#addsinglelink","text":"const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);","title":"addSingleLink"},{"location":"v2/sp/related-items/#addsinglelinktourl","text":"This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document. // get a file's server relative url in some manner, here we add one const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, \"Content\", true).then(r => r.data); // add an item or get an item from the task list const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);","title":"addSingleLinkToUrl"},{"location":"v2/sp/related-items/#addsinglelinkfromurl","text":"This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.","title":"addSingleLinkFromUrl"},{"location":"v2/sp/related-items/#deletesinglelink","text":"This method allows you to delete a link previously created. const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add the link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); // delete the link await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);","title":"deleteSingleLink"},{"location":"v2/sp/related-items/#getrelateditems","text":"Gets the related items for an item import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id); // items.length === 2 Related items are defined by the IRelatedItem interface export interface IRelatedItem { ListId: string; ItemId: number; Url: string; Title: string; WebId: string; IconUrl: string; }","title":"getRelatedItems"},{"location":"v2/sp/related-items/#getpageonerelateditems","text":"Gets an abbreviated set of related items import { IRelatedItem } from \"@pnp/sp/related-items\"; const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl); const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data); // add a link await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl); const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id); // items.length === 2","title":"getPageOneRelatedItems"},{"location":"v2/sp/search/","text":"@pnp/sp/search \u00b6 Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier. Search \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults } from \"@pnp/sp/search\"; Preset: All import { sp, ISearchQuery, SearchResults } from \"@pnp/sp/presets/all\"; Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // text search using SharePoint default values for other parameters const results: SearchResults = await sp.search(\"test\"); console.log(results.ElapsedTime); console.log(results.RowCount); console.log(results.PrimarySearchResults); // define a search query object matching the ISearchQuery interface const results2: SearchResults = await sp.search({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, }); console.log(results2.ElapsedTime); console.log(results2.RowCount); console.log(results2.PrimarySearchResults); // define a query using a builder const builder = SearchQueryBuilder(\"test\").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites; const results3 = await sp.search(builder); console.log(results3.ElapsedTime); console.log(results3.RowCount); console.log(results3.PrimarySearchResults); Search Result Caching \u00b6 You can use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace \"search\" with \"searchWithCaching\" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; sp.searchWithCaching({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, } as ISearchQuery).then((r: SearchResults) => { console.log(r.ElapsedTime); console.log(r.RowCount); console.log(r.PrimarySearchResults); }); // use a query builder const builder = SearchQueryBuilder(\"test\").rowLimit(3); // supply a search query builder and caching options const results2 = await sp.searchWithCaching(builder, { key: \"mykey\", expiration: dateAdd(new Date(), \"month\", 1) }); console.log(results2.TotalRows); Paging with SearchResults.getPage \u00b6 Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // this will hold our current results let currentResults: SearchResults = null; let page = 1; // triggered on page load or through some other means function onStart() { // construct our query that will be used throughout the paging process, likely from user input const q = SearchQueryBuilder(\"test\").rowLimit(5); const results = await sp.search(q); currentResults = results; // set the current results page = 1; // reset page counter // update UI... } // triggered by an event async function next() { currentResults = await currentResults.getPage(++page); // update UI... } // triggered by an event async function prev() { currentResults = await currentResults.getPage(--page); // update UI... } SearchQueryBuilder \u00b6 The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchQueryBuilder, SearchResults, ISearchQuery } from \"@pnp/sp/search\"; // basic usage let q = SearchQueryBuilder().text(\"test\").rowLimit(4).enablePhonetic; sp.search(q).then(h => { /* ... */ }); // provide a default query text at creation let q2 = SearchQueryBuilder(\"text\").rowLimit(4).enablePhonetic; const results: SearchResults = await sp.search(q2); // provide query text and a template for // shared settings across queries that can // be overwritten by individual builders const appSearchSettings: ISearchQuery = { EnablePhonetic: true, HiddenConstraints: \"reports\" }; let q3 = SearchQueryBuilder(\"test\", appSearchSettings).enableQueryRules; let q4 = SearchQueryBuilder(\"financial data\", appSearchSettings).enableSorting.enableStemming; const results2 = await sp.search(q3); const results3 = sp.search(q4); Search Suggest \u00b6 Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISuggestQuery, ISuggestResult } from \"@pnp/sp/search\"; const results = await sp.searchSuggest(\"test\"); const results2 = await sp.searchSuggest({ querytext: \"test\", count: 5, } as ISuggestQuery); Search Factory \u00b6 You can also configure a search or suggest query against any valid SP url using the factory methods. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { Search, Suggest } from \"@pnp/sp/search\"; // set the url for search const searcher = Search(\"https://mytenant.sharepoint.com/sites/dev\"); // this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder) const results = await searcher(\"test\"); // you can reuse the ISearch instance const results2 = await searcher(\"another query\"); // same process works for Suggest const suggester = Suggest(\"https://mytenant.sharepoint.com/sites/dev\"); const suggestions = await suggester({ querytext: \"test\" });","title":"@pnp/sp/search"},{"location":"v2/sp/search/#pnpspsearch","text":"Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.","title":"@pnp/sp/search"},{"location":"v2/sp/search/#search","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults } from \"@pnp/sp/search\"; Preset: All import { sp, ISearchQuery, SearchResults } from \"@pnp/sp/presets/all\"; Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // text search using SharePoint default values for other parameters const results: SearchResults = await sp.search(\"test\"); console.log(results.ElapsedTime); console.log(results.RowCount); console.log(results.PrimarySearchResults); // define a search query object matching the ISearchQuery interface const results2: SearchResults = await sp.search({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, }); console.log(results2.ElapsedTime); console.log(results2.RowCount); console.log(results2.PrimarySearchResults); // define a query using a builder const builder = SearchQueryBuilder(\"test\").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites; const results3 = await sp.search(builder); console.log(results3.ElapsedTime); console.log(results3.RowCount); console.log(results3.PrimarySearchResults);","title":"Search"},{"location":"v2/sp/search/#search-result-caching","text":"You can use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace \"search\" with \"searchWithCaching\" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISearchQuery, SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; sp.searchWithCaching({ Querytext: \"test\", RowLimit: 10, EnableInterleaving: true, } as ISearchQuery).then((r: SearchResults) => { console.log(r.ElapsedTime); console.log(r.RowCount); console.log(r.PrimarySearchResults); }); // use a query builder const builder = SearchQueryBuilder(\"test\").rowLimit(3); // supply a search query builder and caching options const results2 = await sp.searchWithCaching(builder, { key: \"mykey\", expiration: dateAdd(new Date(), \"month\", 1) }); console.log(results2.TotalRows);","title":"Search Result Caching"},{"location":"v2/sp/search/#paging-with-searchresultsgetpage","text":"Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchResults, SearchQueryBuilder } from \"@pnp/sp/search\"; // this will hold our current results let currentResults: SearchResults = null; let page = 1; // triggered on page load or through some other means function onStart() { // construct our query that will be used throughout the paging process, likely from user input const q = SearchQueryBuilder(\"test\").rowLimit(5); const results = await sp.search(q); currentResults = results; // set the current results page = 1; // reset page counter // update UI... } // triggered by an event async function next() { currentResults = await currentResults.getPage(++page); // update UI... } // triggered by an event async function prev() { currentResults = await currentResults.getPage(--page); // update UI... }","title":"Paging with SearchResults.getPage"},{"location":"v2/sp/search/#searchquerybuilder","text":"The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { SearchQueryBuilder, SearchResults, ISearchQuery } from \"@pnp/sp/search\"; // basic usage let q = SearchQueryBuilder().text(\"test\").rowLimit(4).enablePhonetic; sp.search(q).then(h => { /* ... */ }); // provide a default query text at creation let q2 = SearchQueryBuilder(\"text\").rowLimit(4).enablePhonetic; const results: SearchResults = await sp.search(q2); // provide query text and a template for // shared settings across queries that can // be overwritten by individual builders const appSearchSettings: ISearchQuery = { EnablePhonetic: true, HiddenConstraints: \"reports\" }; let q3 = SearchQueryBuilder(\"test\", appSearchSettings).enableQueryRules; let q4 = SearchQueryBuilder(\"financial data\", appSearchSettings).enableSorting.enableStemming; const results2 = await sp.search(q3); const results3 = sp.search(q4);","title":"SearchQueryBuilder"},{"location":"v2/sp/search/#search-suggest","text":"Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { ISuggestQuery, ISuggestResult } from \"@pnp/sp/search\"; const results = await sp.searchSuggest(\"test\"); const results2 = await sp.searchSuggest({ querytext: \"test\", count: 5, } as ISuggestQuery);","title":"Search Suggest"},{"location":"v2/sp/search/#search-factory","text":"You can also configure a search or suggest query against any valid SP url using the factory methods. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/search\"; import { Search, Suggest } from \"@pnp/sp/search\"; // set the url for search const searcher = Search(\"https://mytenant.sharepoint.com/sites/dev\"); // this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder) const results = await searcher(\"test\"); // you can reuse the ISearch instance const results2 = await searcher(\"another query\"); // same process works for Suggest const suggester = Suggest(\"https://mytenant.sharepoint.com/sites/dev\"); const suggestions = await suggester({ querytext: \"test\" });","title":"Search Factory"},{"location":"v2/sp/security/","text":"@pnp/sp/security \u00b6 There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below. Site permissions are managed on the root web of the site collection. A Note on Selective Imports for Security \u00b6 Because the method are shared you can opt to import only the methods for one of the instances. import \"@pnp/sp/security/web\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/security/item\"; Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module: import \"@pnp/sp/security\"; Securable Methods \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // role assignments (see section below) await list.roleAssignments(); // data will represent one of the possible parents Site, Web, or List const data = await list.firstUniqueAncestorSecurableObject(); // getUserEffectivePermissions const users = await sp.web.siteUsers.top(1).select(\"LoginName\")(); const perms = await list.getUserEffectivePermissions(users[0].LoginName); // getCurrentUserEffectivePermissions const perms2 = list.getCurrentUserEffectivePermissions(); // userHasPermissions const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems) // currentUserHasPermissions const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems) // breakRoleInheritance await list.breakRoleInheritance(); // copy existing permissions await list.breakRoleInheritance(true); // copy existing permissions and reset all child securables to the new permissions await list.breakRoleInheritance(true, true); // resetRoleInheritance await list.resetRoleInheritance(); Web Specific methods \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // role definitions (see section below) const defs = await sp.web.roleDefinitions(); Role Assignments \u00b6 Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/web\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // list role assignments const assignments = await list.roleAssignments(); // add a role assignment const defs = await sp.web.roleDefinitions(); const user = await sp.web.currentUser(); const r = await list.roleAssignments.add(user.Id, defs[0].Id); // remove a role assignment const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); const r = await list.roleAssignments.remove(ra.Id); // read role assignment info const info = await list.roleAssignments.getById(ra.Id)(); // get the groups const info2 = await list.roleAssignments.getById(ra.Id).groups(); // get the bindings const info3 = await list.roleAssignments.getById(ra.Id).bindings(); // delete a role assignment (same as remove) const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); // delete it await list.roleAssignments.getById(ra.Id).delete(); Role Definitions \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // read role definitions const defs = await sp.web.roleDefinitions(); // get by id const def = await sp.web.roleDefinitions.getById(5)(); const def = await sp.web.roleDefinitions.getById(5).select(\"Name\", \"Order\")(); // get by name const def = await sp.web.roleDefinitions.getByName(\"Full Control\")(); const def = await sp.web.roleDefinitions.getByName(\"Full Control\").select(\"Name\", \"Order\")(); // get by type const def = await sp.web.roleDefinitions.getByName(5)(); const def = await sp.web.roleDefinitions.getByName(5).select(\"Name\", \"Order\")(); // add // name The new role definition's name // description The new role definition's description // order The order in which the role definition appears // basePermissions The permissions mask for this role definition const rdar = await sp.web.roleDefinitions.add(\"title\", \"description\", 99, { High: 1, Low: 2 }); // the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example // delete await sp.web.roleDefinitions.getById(5).delete(); // update const res = sp.web.roleDefinitions.getById(5).update({ Name: \"New Name\" });","title":"@pnp/sp/security"},{"location":"v2/sp/security/#pnpspsecurity","text":"There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below. Site permissions are managed on the root web of the site collection.","title":"@pnp/sp/security"},{"location":"v2/sp/security/#a-note-on-selective-imports-for-security","text":"Because the method are shared you can opt to import only the methods for one of the instances. import \"@pnp/sp/security/web\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/security/item\"; Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module: import \"@pnp/sp/security\";","title":"A Note on Selective Imports for Security"},{"location":"v2/sp/security/#securable-methods","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/list\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // role assignments (see section below) await list.roleAssignments(); // data will represent one of the possible parents Site, Web, or List const data = await list.firstUniqueAncestorSecurableObject(); // getUserEffectivePermissions const users = await sp.web.siteUsers.top(1).select(\"LoginName\")(); const perms = await list.getUserEffectivePermissions(users[0].LoginName); // getCurrentUserEffectivePermissions const perms2 = list.getCurrentUserEffectivePermissions(); // userHasPermissions const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems) // currentUserHasPermissions const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems) // breakRoleInheritance await list.breakRoleInheritance(); // copy existing permissions await list.breakRoleInheritance(true); // copy existing permissions and reset all child securables to the new permissions await list.breakRoleInheritance(true, true); // resetRoleInheritance await list.resetRoleInheritance();","title":"Securable Methods"},{"location":"v2/sp/security/#web-specific-methods","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // role definitions (see section below) const defs = await sp.web.roleDefinitions();","title":"Web Specific methods"},{"location":"v2/sp/security/#role-assignments","text":"Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/security/web\"; import \"@pnp/sp/site-users/web\"; import { IList } from \"@pnp/sp/lists\"; import { PermissionKind } from \"@pnp/sp/security\"; // ensure we have a list const ler = await sp.web.lists.ensure(\"SecurityTestingList\"); const list: IList = ler.list; // list role assignments const assignments = await list.roleAssignments(); // add a role assignment const defs = await sp.web.roleDefinitions(); const user = await sp.web.currentUser(); const r = await list.roleAssignments.add(user.Id, defs[0].Id); // remove a role assignment const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); const r = await list.roleAssignments.remove(ra.Id); // read role assignment info const info = await list.roleAssignments.getById(ra.Id)(); // get the groups const info2 = await list.roleAssignments.getById(ra.Id).groups(); // get the bindings const info3 = await list.roleAssignments.getById(ra.Id).bindings(); // delete a role assignment (same as remove) const ras = await list.roleAssignments(); // filter/find the role assignment you want to remove // here we just grab the first const ra = ras.find(v => true); // delete it await list.roleAssignments.getById(ra.Id).delete();","title":"Role Assignments"},{"location":"v2/sp/security/#role-definitions","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/security/web\"; // read role definitions const defs = await sp.web.roleDefinitions(); // get by id const def = await sp.web.roleDefinitions.getById(5)(); const def = await sp.web.roleDefinitions.getById(5).select(\"Name\", \"Order\")(); // get by name const def = await sp.web.roleDefinitions.getByName(\"Full Control\")(); const def = await sp.web.roleDefinitions.getByName(\"Full Control\").select(\"Name\", \"Order\")(); // get by type const def = await sp.web.roleDefinitions.getByName(5)(); const def = await sp.web.roleDefinitions.getByName(5).select(\"Name\", \"Order\")(); // add // name The new role definition's name // description The new role definition's description // order The order in which the role definition appears // basePermissions The permissions mask for this role definition const rdar = await sp.web.roleDefinitions.add(\"title\", \"description\", 99, { High: 1, Low: 2 }); // the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example // delete await sp.web.roleDefinitions.getById(5).delete(); // update const res = sp.web.roleDefinitions.getById(5).update({ Name: \"New Name\" });","title":"Role Definitions"},{"location":"v2/sp/sharing/","text":"@pnp/sp/sharing \u00b6 Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online. One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue. Imports \u00b6 In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects. Import All \u00b6 To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module: import \"@pnp/sp/sharing\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName); Selective Import \u00b6 Import only the web's sharing methods into the library import \"@pnp/sp/sharing/web\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName); getShareLink \u00b6 Applies to: Item, Folder, File Creates a sharing link for the given resource with an optional expiration. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { SharingLinkKind, IShareLinkResponse } from \"@pnp/sp/sharing\"; import { dateAdd } from \"@pnp/core\"; const result = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView); console.log(JSON.stringify(result, null, 2)); const result2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), \"day\", 5)); console.log(JSON.stringify(result2, null, 2)); shareWith \u00b6 Applies to: Item, Folder, File, Web Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames . The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/files/web\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; const result = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\"); console.log(JSON.stringify(result, null, 2)); // Share and allow editing const result2 = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit); console.log(JSON.stringify(result2, null, 2)); // share folder const result3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children) await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit, true, true); // Share a file await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share a file with edit permissions await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit); shareObject & shareObjectRaw \u00b6 Applies to: Web Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; // Share an object in this web const result = await sp.web.shareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", \"i:0#.f|membership|user@site.com\", SharingRole.View); // Share an object with all settings available await sp.web.shareObjectRaw({ url: \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", peoplePickerInput: [{ Key: \"i:0#.f|membership|user@site.com\" }], roleValue: \"role: 1973741327\", groupId: 0, propagateAcl: false, sendEmail: true, includeAnonymousLinkInEmail: false, emailSubject: \"subject\", emailBody: \"body\", useSimplifiedRoles: true, }); unshareObject \u00b6 Applies to: Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result = await sp.web.unshareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\"); checkSharingPermissions \u00b6 Applies to: Item, Folder, File Checks Permissions on the list of Users and returns back role the users have on the Item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing/folders\"; import \"@pnp/sp/folders/web\"; import { SharingEntityPermission } from \"@pnp/sp/sharing\"; // check the sharing permissions for a folder const perms = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").checkSharingPermissions([{ alias: \"i:0#.f|membership|user@site.com\" }]); getSharingInformation \u00b6 Applies to: Item, Folder, File Get Sharing Information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingInformation } from \"@pnp/sp/sharing\"; // Get the sharing information for a folder const info = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation(); getObjectSharingSettings \u00b6 Applies to: Item, Folder, File Gets the sharing settings import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { IObjectSharingSettings } from \"@pnp/sp/sharing\"; // Gets the sharing object settings const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getObjectSharingSettings(); unshare \u00b6 Applies to: Item, Folder, File Unshares a given resource import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshare(); deleteSharingLinkByKind \u00b6 Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult, SharingLinkKind } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit); unshareLink \u00b6 Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { SharingLinkKind } from \"@pnp/sp/sharing\"; await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit); // specify the sharing link id if available await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit, \"12345\");","title":"@pnp/sp/sharing"},{"location":"v2/sp/sharing/#pnpspsharing","text":"Note: This API is still considered \"beta\" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online. One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.","title":"@pnp/sp/sharing"},{"location":"v2/sp/sharing/#imports","text":"In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.","title":"Imports"},{"location":"v2/sp/sharing/#import-all","text":"To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module: import \"@pnp/sp/sharing\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName);","title":"Import All"},{"location":"v2/sp/sharing/#selective-import","text":"Import only the web's sharing methods into the library import \"@pnp/sp/sharing/web\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; import { sp } from \"@pnp/sp\"; const user = await sp.web.siteUsers.getByEmail(\"user@site.com\")(); const r = await sp.web.shareWith(user.LoginName);","title":"Selective Import"},{"location":"v2/sp/sharing/#getsharelink","text":"Applies to: Item, Folder, File Creates a sharing link for the given resource with an optional expiration. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { SharingLinkKind, IShareLinkResponse } from \"@pnp/sp/sharing\"; import { dateAdd } from \"@pnp/core\"; const result = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView); console.log(JSON.stringify(result, null, 2)); const result2 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), \"day\", 5)); console.log(JSON.stringify(result2, null, 2));","title":"getShareLink"},{"location":"v2/sp/sharing/#sharewith","text":"Applies to: Item, Folder, File, Web Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames . The folder method takes an optional parameter \"shareEverything\" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders/web\"; import \"@pnp/sp/files/web\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; const result = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\"); console.log(JSON.stringify(result, null, 2)); // Share and allow editing const result2 = await sp.web.shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit); console.log(JSON.stringify(result2, null, 2)); // share folder const result3 = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/folder1\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children) await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit, true, true); // Share a file await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\"); // Share a file with edit permissions await sp.web.getFileByServerRelativeUrl(\"/sites/dev/Shared Documents/test.txt\").shareWith(\"i:0#.f|membership|user@site.com\", SharingRole.Edit);","title":"shareWith"},{"location":"v2/sp/sharing/#shareobject-shareobjectraw","text":"Applies to: Web Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult, SharingRole } from \"@pnp/sp/sharing\"; // Share an object in this web const result = await sp.web.shareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", \"i:0#.f|membership|user@site.com\", SharingRole.View); // Share an object with all settings available await sp.web.shareObjectRaw({ url: \"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\", peoplePickerInput: [{ Key: \"i:0#.f|membership|user@site.com\" }], roleValue: \"role: 1973741327\", groupId: 0, propagateAcl: false, sendEmail: true, includeAnonymousLinkInEmail: false, emailSubject: \"subject\", emailBody: \"body\", useSimplifiedRoles: true, });","title":"shareObject & shareObjectRaw"},{"location":"v2/sp/sharing/#unshareobject","text":"Applies to: Web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result = await sp.web.unshareObject(\"https://mysite.sharepoint.com/sites/dev/Docs/test.txt\");","title":"unshareObject"},{"location":"v2/sp/sharing/#checksharingpermissions","text":"Applies to: Item, Folder, File Checks Permissions on the list of Users and returns back role the users have on the Item. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing/folders\"; import \"@pnp/sp/folders/web\"; import { SharingEntityPermission } from \"@pnp/sp/sharing\"; // check the sharing permissions for a folder const perms = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").checkSharingPermissions([{ alias: \"i:0#.f|membership|user@site.com\" }]);","title":"checkSharingPermissions"},{"location":"v2/sp/sharing/#getsharinginformation","text":"Applies to: Item, Folder, File Get Sharing Information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingInformation } from \"@pnp/sp/sharing\"; // Get the sharing information for a folder const info = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getSharingInformation();","title":"getSharingInformation"},{"location":"v2/sp/sharing/#getobjectsharingsettings","text":"Applies to: Item, Folder, File Gets the sharing settings import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { IObjectSharingSettings } from \"@pnp/sp/sharing\"; // Gets the sharing object settings const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").getObjectSharingSettings();","title":"getObjectSharingSettings"},{"location":"v2/sp/sharing/#unshare","text":"Applies to: Item, Folder, File Unshares a given resource import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshare();","title":"unshare"},{"location":"v2/sp/sharing/#deletesharinglinkbykind","text":"Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { ISharingResult, SharingLinkKind } from \"@pnp/sp/sharing\"; const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);","title":"deleteSharingLinkByKind"},{"location":"v2/sp/sharing/#unsharelink","text":"Applies to: Item, Folder, File import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sharing\"; import \"@pnp/sp/folders\"; import { SharingLinkKind } from \"@pnp/sp/sharing\"; await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit); // specify the sharing link id if available await sp.web.getFolderByServerRelativeUrl(\"/sites/dev/Shared Documents/test\").unshareLink(SharingLinkKind.AnonymousEdit, \"12345\");","title":"unshareLink"},{"location":"v2/sp/site-designs/","text":"@pnp/sp/site-designs \u00b6 You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information. Site Designs \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Create a new site design \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // WebTemplate: 64 Team site template, 68 Communication site template const siteDesign = await sp.siteDesigns.createSiteDesign({ SiteScriptIds: [\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"], Title: \"SiteDesign001\", WebTemplate: \"64\", }); console.log(siteDesign.Title); Applying a site design to a site \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Limited to 30 actions in a site script, but runs synchronously await sp.siteDesigns.applySiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\",\"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\"); // Better use the following method for 300 actions in a site script const task = await sp.web.addSiteDesignTask(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); Retrieval \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Retrieving all site designs const allSiteDesigns = await sp.siteDesigns.getSiteDesigns(); console.log(`Total site designs: ${allSiteDesigns.length}`); // Retrieving a single site design by Id const siteDesign = await sp.siteDesigns.getSiteDesignMetadata(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(siteDesign.Title); Update and delete \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Update const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", Title: \"SiteDesignUpdatedTitle001\" }); // Delete await sp.siteDesigns.deleteSiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); Setting Rights/Permissions \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Get const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(rights.length > 0 ? rights[0].PrincipalName : \"\"); // Grant await sp.siteDesigns.grantSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Revoke await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Reset all view rights const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", rights.map(u => u.PrincipalName)); Get a history of site designs that have run on a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; const runs = await sp.web.getSiteDesignRuns(); const runs2 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\"); // Get runs specific to a site design const runs3 = await sp.web.getSiteDesignRuns(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); const runs4 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\", \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); // For more information about the site script actions const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID); const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus(\"https://TENANT.sharepoint.com/sites/mysite\", runs[0].ID);","title":"@pnp/sp/site-designs"},{"location":"v2/sp/site-designs/#pnpspsite-designs","text":"You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. Check out SharePoint site design and site script overview for more information.","title":"@pnp/sp/site-designs"},{"location":"v2/sp/site-designs/#site-designs","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"Site Designs"},{"location":"v2/sp/site-designs/#create-a-new-site-design","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // WebTemplate: 64 Team site template, 68 Communication site template const siteDesign = await sp.siteDesigns.createSiteDesign({ SiteScriptIds: [\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"], Title: \"SiteDesign001\", WebTemplate: \"64\", }); console.log(siteDesign.Title);","title":"Create a new site design"},{"location":"v2/sp/site-designs/#applying-a-site-design-to-a-site","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Limited to 30 actions in a site script, but runs synchronously await sp.siteDesigns.applySiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\",\"https://contoso.sharepoint.com/sites/teamsite-pnpjs001\"); // Better use the following method for 300 actions in a site script const task = await sp.web.addSiteDesignTask(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");","title":"Applying a site design to a site"},{"location":"v2/sp/site-designs/#retrieval","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Retrieving all site designs const allSiteDesigns = await sp.siteDesigns.getSiteDesigns(); console.log(`Total site designs: ${allSiteDesigns.length}`); // Retrieving a single site design by Id const siteDesign = await sp.siteDesigns.getSiteDesignMetadata(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(siteDesign.Title);","title":"Retrieval"},{"location":"v2/sp/site-designs/#update-and-delete","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Update const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", Title: \"SiteDesignUpdatedTitle001\" }); // Delete await sp.siteDesigns.deleteSiteDesign(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\");","title":"Update and delete"},{"location":"v2/sp/site-designs/#setting-rightspermissions","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; // Get const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); console.log(rights.length > 0 ? rights[0].PrincipalName : \"\"); // Grant await sp.siteDesigns.grantSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Revoke await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", [\"user@contoso.onmicrosoft.com\"]); // Reset all view rights const rights = await sp.siteDesigns.getSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); await sp.siteDesigns.revokeSiteDesignRights(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\", rights.map(u => u.PrincipalName));","title":"Setting Rights/Permissions"},{"location":"v2/sp/site-designs/#get-a-history-of-site-designs-that-have-run-on-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-designs\"; const runs = await sp.web.getSiteDesignRuns(); const runs2 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\"); // Get runs specific to a site design const runs3 = await sp.web.getSiteDesignRuns(\"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); const runs4 = await sp.siteDesigns.getSiteDesignRun(\"https://TENANT.sharepoint.com/sites/mysite\", \"75b9d8fe-4381-45d9-88c6-b03f483ae6a8\"); // For more information about the site script actions const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID); const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus(\"https://TENANT.sharepoint.com/sites/mysite\", runs[0].ID);","title":"Get a history of site designs that have run on a web"},{"location":"v2/sp/site-groups/","text":"@pnp/sp/site-groups \u00b6 The site groups module provides methods to manage groups for a sharepoint site. ISiteGroups \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups } from \"@pnp/sp/presets/all\"; Get all site groups \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // gets all site groups of the web const groups = await sp.web.siteGroups(); Get the associated groups of a web \u00b6 You can get the associated Owner, Member and Visitor groups of a web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Gets the associated visitors group of a web const visitorGroup = await sp.web.associatedVisitorGroup(); // Gets the associated members group of a web const memberGroup = await sp.web.associatedMemberGroup(); // Gets the associated owners group of a web const ownerGroup = await sp.web.associatedOwnerGroup(); Create the default associated groups for a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Breaks permission inheritance and creates the default associated groups for the web // Login name of the owner const owner1 = \"owner@example.onmicrosoft.com\"; // Specify true, the permissions should be copied from the current parent scope, else false const copyRoleAssignments = false; // Specify true to make all child securable objects inherit role assignments from the current object const clearSubScopes = true; await sp.web.createDefaultAssociatedGroups(\"PnP Site\", owner1, copyRoleAssignments, clearSubScopes); Create a new site group \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Creates a new site group with the specified title await sp.web.siteGroups.add({\"Title\":\"new group name\"}); ISiteGroup \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups, SiteGroup } from \"@pnp/sp/presets/all\"; Getting and updating the groups of a sharepoint web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get the group using a group id const groupID = 33; let grp = await sp.web.siteGroups.getById(groupID)(); // get the group using the group's name const groupName = \"ClassicTeam Visitors\"; grp = await sp.web.siteGroups.getByName(groupName)(); // update a group await sp.web.siteGroups.getById(groupID).update({\"Title\": \"New Group Title\"}); // delete a group from the site using group id await sp.web.siteGroups.removeById(groupID); // delete a group from the site using group name await sp.web.siteGroups.removeByLoginName(groupName); Getting all users of a group \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get all users of group const groupID = 7; const users = await sp.web.siteGroups.getById(groupID).users(); Updating the owner of a site group \u00b6 Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // Update the owner with a user id await sp.web.siteGroups.getById(7).setUserAsOwner(4);","title":"@pnp/sp/site-groups"},{"location":"v2/sp/site-groups/#pnpspsite-groups","text":"The site groups module provides methods to manage groups for a sharepoint site.","title":"@pnp/sp/site-groups"},{"location":"v2/sp/site-groups/#isitegroups","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups } from \"@pnp/sp/presets/all\";","title":"ISiteGroups"},{"location":"v2/sp/site-groups/#get-all-site-groups","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // gets all site groups of the web const groups = await sp.web.siteGroups();","title":"Get all site groups"},{"location":"v2/sp/site-groups/#get-the-associated-groups-of-a-web","text":"You can get the associated Owner, Member and Visitor groups of a web import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Gets the associated visitors group of a web const visitorGroup = await sp.web.associatedVisitorGroup(); // Gets the associated members group of a web const memberGroup = await sp.web.associatedMemberGroup(); // Gets the associated owners group of a web const ownerGroup = await sp.web.associatedOwnerGroup();","title":"Get the associated groups of a web"},{"location":"v2/sp/site-groups/#create-the-default-associated-groups-for-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Breaks permission inheritance and creates the default associated groups for the web // Login name of the owner const owner1 = \"owner@example.onmicrosoft.com\"; // Specify true, the permissions should be copied from the current parent scope, else false const copyRoleAssignments = false; // Specify true to make all child securable objects inherit role assignments from the current object const clearSubScopes = true; await sp.web.createDefaultAssociatedGroups(\"PnP Site\", owner1, copyRoleAssignments, clearSubScopes);","title":"Create the default associated groups for a web"},{"location":"v2/sp/site-groups/#create-a-new-site-group","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; // Creates a new site group with the specified title await sp.web.siteGroups.add({\"Title\":\"new group name\"});","title":"Create a new site group"},{"location":"v2/sp/site-groups/#isitegroup","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups/web\"; Preset: All import {sp, SiteGroups, SiteGroup } from \"@pnp/sp/presets/all\";","title":"ISiteGroup"},{"location":"v2/sp/site-groups/#getting-and-updating-the-groups-of-a-sharepoint-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get the group using a group id const groupID = 33; let grp = await sp.web.siteGroups.getById(groupID)(); // get the group using the group's name const groupName = \"ClassicTeam Visitors\"; grp = await sp.web.siteGroups.getByName(groupName)(); // update a group await sp.web.siteGroups.getById(groupID).update({\"Title\": \"New Group Title\"}); // delete a group from the site using group id await sp.web.siteGroups.removeById(groupID); // delete a group from the site using group name await sp.web.siteGroups.removeByLoginName(groupName);","title":"Getting and updating the groups of a sharepoint web"},{"location":"v2/sp/site-groups/#getting-all-users-of-a-group","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // get all users of group const groupID = 7; const users = await sp.web.siteGroups.getById(groupID).users();","title":"Getting all users of a group"},{"location":"v2/sp/site-groups/#updating-the-owner-of-a-site-group","text":"Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-groups\"; // Update the owner with a user id await sp.web.siteGroups.getById(7).setUserAsOwner(4);","title":"Updating the owner of a site group"},{"location":"v2/sp/site-scripts/","text":"@pnp/sp/site-scripts \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Create a new site script \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const sitescriptContent = { \"$schema\": \"schema.json\", \"actions\": [ { \"themeName\": \"Theme Name 123\", \"verb\": \"applyTheme\", }, ], \"bindata\": {}, \"version\": 1, }; const siteScript = await sp.siteScripts.createSiteScript(\"Title\", \"description\", sitescriptContent); console.log(siteScript.Title); Retrieval \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Retrieving all site scripts const allSiteScripts = await sp.siteScripts.getSiteScripts(); console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : \"\"); // Retrieving a single site script by Id const siteScript = await sp.siteScripts.getSiteScriptMetadata(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"); console.log(siteScript.Title); Update and delete \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Update const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\", Title: \"New Title\" }); console.log(updatedSiteScript.Title); // Delete await sp.siteScripts.deleteSiteScript(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"); Get site script from a list \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Using the absolute URL of the list const ss = await sp.siteScripts.getSiteScriptFromList(\"https://TENANT.sharepoint.com/Lists/mylist\"); // Using the PnPjs web object to fetch the site script from a specific list const ss2 = await sp.web.lists.getByTitle(\"mylist\").getSiteScript(); Get site script from a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const extractInfo = { IncludeBranding: true, IncludeLinksToExportedItems: true, IncludeRegionalSettings: true, IncludeSiteExternalSharingCapability: true, IncludeTheme: true, IncludedLists: [\"Lists/MyList\"] }; const ss = await sp.siteScripts.getSiteScriptFromWeb(\"https://TENANT.sharepoint.com/sites/mysite\", extractInfo); // Using the PnPjs web object to fetch the site script from a specific web const ss2 = await sp.web.getSiteScript(extractInfo); Execute Site Script Action \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const ss = await sp.siteScripts.executeSiteScriptAction(siteScript); Execute site script for a specific web \u00b6 import { sp } from \"@pnp/sp\"; import { SiteScripts } \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const scriptService = SiteScripts(\"https://absolute/url/to/web\"); const ss = await scriptService.executeSiteScriptAction(siteScript);","title":"@pnp/sp/site-scripts"},{"location":"v2/sp/site-scripts/#pnpspsite-scripts","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"@pnp/sp/site-scripts"},{"location":"v2/sp/site-scripts/#create-a-new-site-script","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const sitescriptContent = { \"$schema\": \"schema.json\", \"actions\": [ { \"themeName\": \"Theme Name 123\", \"verb\": \"applyTheme\", }, ], \"bindata\": {}, \"version\": 1, }; const siteScript = await sp.siteScripts.createSiteScript(\"Title\", \"description\", sitescriptContent); console.log(siteScript.Title);","title":"Create a new site script"},{"location":"v2/sp/site-scripts/#retrieval","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Retrieving all site scripts const allSiteScripts = await sp.siteScripts.getSiteScripts(); console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : \"\"); // Retrieving a single site script by Id const siteScript = await sp.siteScripts.getSiteScriptMetadata(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\"); console.log(siteScript.Title);","title":"Retrieval"},{"location":"v2/sp/site-scripts/#update-and-delete","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Update const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: \"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\", Title: \"New Title\" }); console.log(updatedSiteScript.Title); // Delete await sp.siteScripts.deleteSiteScript(\"884ed56b-1aab-4653-95cf-4be0bfa5ef0a\");","title":"Update and delete"},{"location":"v2/sp/site-scripts/#get-site-script-from-a-list","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; // Using the absolute URL of the list const ss = await sp.siteScripts.getSiteScriptFromList(\"https://TENANT.sharepoint.com/Lists/mylist\"); // Using the PnPjs web object to fetch the site script from a specific list const ss2 = await sp.web.lists.getByTitle(\"mylist\").getSiteScript();","title":"Get site script from a list"},{"location":"v2/sp/site-scripts/#get-site-script-from-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const extractInfo = { IncludeBranding: true, IncludeLinksToExportedItems: true, IncludeRegionalSettings: true, IncludeSiteExternalSharingCapability: true, IncludeTheme: true, IncludedLists: [\"Lists/MyList\"] }; const ss = await sp.siteScripts.getSiteScriptFromWeb(\"https://TENANT.sharepoint.com/sites/mysite\", extractInfo); // Using the PnPjs web object to fetch the site script from a specific web const ss2 = await sp.web.getSiteScript(extractInfo);","title":"Get site script from a web"},{"location":"v2/sp/site-scripts/#execute-site-script-action","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const ss = await sp.siteScripts.executeSiteScriptAction(siteScript);","title":"Execute Site Script Action"},{"location":"v2/sp/site-scripts/#execute-site-script-for-a-specific-web","text":"import { sp } from \"@pnp/sp\"; import { SiteScripts } \"@pnp/sp/site-scripts\"; const siteScript = \"your site script action...\"; const scriptService = SiteScripts(\"https://absolute/url/to/web\"); const ss = await scriptService.executeSiteScriptAction(siteScript);","title":"Execute site script for a specific web"},{"location":"v2/sp/site-users/","text":"@pnp/sp/site-users \u00b6 The site users module provides methods to manage users for a sharepoint site. ISiteUsers \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers } from \"@pnp/sp/presets/all\"; Get all site user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const users = await sp.web.siteUsers(); Get Current user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let user = await sp.web.currentUser(); Get user by id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const id = 6; user = await sp.web.getUserById(id); Ensure user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const username = \"usernames@microsoft.com\"; result = await sp.web.ensureUser(username); ISiteUser \u00b6 Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers, SiteUser } from \"@pnp/sp/presets/all\"; Get user Groups \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let groups = await sp.web.currentUser.groups(); Add user to Site collection \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const user = await sp.web.ensureUser(\"userLoginname\") const users = await sp.web.siteUsers; await users.add(user.data.LoginName); Get user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // get user object by id const user = await sp.web.siteUsers.getById(6); //get user object by Email const user = await sp.web.siteUsers.getByEmail(\"user@mail.com\"); //get user object by LoginName const user = await sp.web.siteUsers.getByLoginName(\"userLoginName\"); Update user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let userProps = await sp.web.currentUser(); userProps.Title = \"New title\"; await sp.web.currentUser.update(userProps); Remove user \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // remove user by id await sp.web.siteUsers.removeById(6); // remove user by LoginName await sp.web.siteUsers.removeByLoginName(6); ISiteUserProps \u00b6 User properties: Property Name Type Description Email string Contains Site user email Id Number Contains Site user Id IsHiddenInUI Boolean Site user IsHiddenInUI IsShareByEmailGuestUser boolean Site user is external user IsSiteAdmin Boolean Describes if Site user Is Site Admin LoginName string Site user LoginName PrincipalType number Site user Principal type Title string Site user Title interface ISiteUserProps { /** * Contains Site user email * */ Email: string; /** * Contains Site user Id * */ Id: number; /** * Site user IsHiddenInUI * */ IsHiddenInUI: boolean; /** * Site user IsShareByEmailGuestUser * */ IsShareByEmailGuestUser: boolean; /** * Describes if Site user Is Site Admin * */ IsSiteAdmin: boolean; /** * Site user LoginName * */ LoginName: string; /** * Site user Principal type * */ PrincipalType: number | PrincipalType; /** * Site user Title * */ Title: string; }","title":"@pnp/sp/site-users"},{"location":"v2/sp/site-users/#pnpspsite-users","text":"The site users module provides methods to manage users for a sharepoint site.","title":"@pnp/sp/site-users"},{"location":"v2/sp/site-users/#isiteusers","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers } from \"@pnp/sp/presets/all\";","title":"ISiteUsers"},{"location":"v2/sp/site-users/#get-all-site-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const users = await sp.web.siteUsers();","title":"Get all site user"},{"location":"v2/sp/site-users/#get-current-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let user = await sp.web.currentUser();","title":"Get Current user"},{"location":"v2/sp/site-users/#get-user-by-id","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const id = 6; user = await sp.web.getUserById(id);","title":"Get user by id"},{"location":"v2/sp/site-users/#ensure-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const username = \"usernames@microsoft.com\"; result = await sp.web.ensureUser(username);","title":"Ensure user"},{"location":"v2/sp/site-users/#isiteuser","text":"Scenario Import Statement Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users\"; Selective 3 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; Preset: All import {sp, SiteUsers, SiteUser } from \"@pnp/sp/presets/all\";","title":"ISiteUser"},{"location":"v2/sp/site-users/#get-user-groups","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let groups = await sp.web.currentUser.groups();","title":"Get user Groups"},{"location":"v2/sp/site-users/#add-user-to-site-collection","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; const user = await sp.web.ensureUser(\"userLoginname\") const users = await sp.web.siteUsers; await users.add(user.data.LoginName);","title":"Add user to Site collection"},{"location":"v2/sp/site-users/#get-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // get user object by id const user = await sp.web.siteUsers.getById(6); //get user object by Email const user = await sp.web.siteUsers.getByEmail(\"user@mail.com\"); //get user object by LoginName const user = await sp.web.siteUsers.getByLoginName(\"userLoginName\");","title":"Get user"},{"location":"v2/sp/site-users/#update-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; let userProps = await sp.web.currentUser(); userProps.Title = \"New title\"; await sp.web.currentUser.update(userProps);","title":"Update user"},{"location":"v2/sp/site-users/#remove-user","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/site-users/web\"; // remove user by id await sp.web.siteUsers.removeById(6); // remove user by LoginName await sp.web.siteUsers.removeByLoginName(6);","title":"Remove user"},{"location":"v2/sp/site-users/#isiteuserprops","text":"User properties: Property Name Type Description Email string Contains Site user email Id Number Contains Site user Id IsHiddenInUI Boolean Site user IsHiddenInUI IsShareByEmailGuestUser boolean Site user is external user IsSiteAdmin Boolean Describes if Site user Is Site Admin LoginName string Site user LoginName PrincipalType number Site user Principal type Title string Site user Title interface ISiteUserProps { /** * Contains Site user email * */ Email: string; /** * Contains Site user Id * */ Id: number; /** * Site user IsHiddenInUI * */ IsHiddenInUI: boolean; /** * Site user IsShareByEmailGuestUser * */ IsShareByEmailGuestUser: boolean; /** * Describes if Site user Is Site Admin * */ IsSiteAdmin: boolean; /** * Site user LoginName * */ LoginName: string; /** * Site user Principal type * */ PrincipalType: number | PrincipalType; /** * Site user Title * */ Title: string; }","title":"ISiteUserProps"},{"location":"v2/sp/sites/","text":"@pnp/sp/site - Site properties \u00b6 Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types. Get context information for the current site collection \u00b6 Using the library, you can get the context information of the current site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IContextInfo } from \"@pnp/sp/sites\"; const oContext: IContextInfo = await sp.site.getContextInfo(); console.log(oContext.FormDigestValue); Get document libraries of a web \u00b6 Using the library, you can get a list of the document libraries present in the a given web. Note: Works only in SharePoint online import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IDocumentLibraryInformation } from \"@pnp/sp/sites\"; const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries(\"https://tenant.sharepoint.com/sites/test/subsite\"); //we got the array of document library information docLibs.forEach((docLib: IDocumentLibraryInformation) => { // do something with each library }); Open Web By Id \u00b6 Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const w = await sp.site.openWebById(\"111ca453-90f5-482e-a381-cee1ff383c9e\"); //we got all the data from the web as well console.log(w.data); // we can chain const w2 = await w.web.select(\"Title\")(); Get site collection url from page \u00b6 Using the library, you can get the site collection url by providing a page url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const d: string = await sp.site.getWebUrlFromPageUrl(\"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\"); console.log(d); //https://tenant.sharepoint.com/sites/test Access the root web \u00b6 There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. \"_api/sites/rootweb\" which does not work for all operations. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; // use for rootweb information access const rootwebData = await sp.site.rootWeb(); // use for chaining const rootweb = await sp.site.getRootWeb(); const listData = await rootWeb.lists.getByTitle(\"MyList\")(); Create a modern communication site \u00b6 Note: Works only in SharePoint online Creates a modern communication site. Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection ) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site Owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createCommunicationSite( \"Title\", 1033, true, \"https://tenant.sharepoint.com/sites/commSite\", \"Description\", \"HBI\", \"f6cc5403-0d63-442e-96c0-285923709ffc\", \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"user@TENANT.onmicrosoft.com\"); Create from Props \u00b6 You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createCommunicationSiteFromProps({ Owner: \"patrick@three18studios.com\", Title: \"A Test Site\", Url: \"https://{tenant}.sharepoint.com/sites/commsite2\", WebTemplate: \"STS#3\", }); Create a modern team site \u00b6 Note: Works only in SharePoint online. It wont work with App only tokens Creates a modern team site backed by O365 group. Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createModernTeamSite( \"displayName\", \"alias\", true, 1033, \"description\", \"HBI\", [\"user1@tenant.onmicrosoft.com\",\"user2@tenant.onmicrosoft.com\",\"user3@tenant.onmicrosoft.com\"], \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"f6cc5403-0d63-442e-96c0-285923709ffc\" ); console.log(d); Create from Props \u00b6 You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createModernTeamSiteFromProps({ alias: \"JenniferGarner\", displayName: \"A Test Site\", owners: [\"patrick@three18studios.com\"], }); Delete a site collection \u00b6 Using the library, you can delete a specific site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { Site } from \"@pnp/sp/sites\"; // Delete the current site await sp.site.delete(); // Specify which site to delete const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const site2 = Site(siteUrl); await site2.delete(); Check if a Site Collection Exists \u00b6 Using the library, you can check if a specific site collection exist or not on your tenant import { sp } from \"@pnp/sp\"; // Specify which site to verify const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const exists = sp.site.exists(siteUrl); console.log(exists);","title":"@pnp/sp/site - Site properties"},{"location":"v2/sp/sites/#pnpspsite-site-properties","text":"Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.","title":"@pnp/sp/site - Site properties"},{"location":"v2/sp/sites/#get-context-information-for-the-current-site-collection","text":"Using the library, you can get the context information of the current site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IContextInfo } from \"@pnp/sp/sites\"; const oContext: IContextInfo = await sp.site.getContextInfo(); console.log(oContext.FormDigestValue);","title":"Get context information for the current site collection"},{"location":"v2/sp/sites/#get-document-libraries-of-a-web","text":"Using the library, you can get a list of the document libraries present in the a given web. Note: Works only in SharePoint online import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { IDocumentLibraryInformation } from \"@pnp/sp/sites\"; const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries(\"https://tenant.sharepoint.com/sites/test/subsite\"); //we got the array of document library information docLibs.forEach((docLib: IDocumentLibraryInformation) => { // do something with each library });","title":"Get document libraries of a web"},{"location":"v2/sp/sites/#open-web-by-id","text":"Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const w = await sp.site.openWebById(\"111ca453-90f5-482e-a381-cee1ff383c9e\"); //we got all the data from the web as well console.log(w.data); // we can chain const w2 = await w.web.select(\"Title\")();","title":"Open Web By Id"},{"location":"v2/sp/sites/#get-site-collection-url-from-page","text":"Using the library, you can get the site collection url by providing a page url import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const d: string = await sp.site.getWebUrlFromPageUrl(\"https://tenant.sharepoint.com/sites/test/Pages/test.aspx\"); console.log(d); //https://tenant.sharepoint.com/sites/test","title":"Get site collection url from page"},{"location":"v2/sp/sites/#access-the-root-web","text":"There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. \"_api/sites/rootweb\" which does not work for all operations. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; // use for rootweb information access const rootwebData = await sp.site.rootWeb(); // use for chaining const rootweb = await sp.site.getRootWeb(); const listData = await rootWeb.lists.getByTitle(\"MyList\")();","title":"Access the root web"},{"location":"v2/sp/sites/#create-a-modern-communication-site","text":"Note: Works only in SharePoint online Creates a modern communication site. Property Type Required Description Title string yes The title of the site to create. lcid number yes The default language to use for the site. shareByEmailEnabled boolean yes If set to true, it will enable sharing files via Email. By default it is set to false url string yes The fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection ) of the site. description string no The description of the communication site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc hubSiteId string no The Guid of the already existing Hub site Owner string no Required when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createCommunicationSite( \"Title\", 1033, true, \"https://tenant.sharepoint.com/sites/commSite\", \"Description\", \"HBI\", \"f6cc5403-0d63-442e-96c0-285923709ffc\", \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"user@TENANT.onmicrosoft.com\");","title":"Create a modern communication site"},{"location":"v2/sp/sites/#create-from-props","text":"You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createCommunicationSiteFromProps({ Owner: \"patrick@three18studios.com\", Title: \"A Test Site\", Url: \"https://{tenant}.sharepoint.com/sites/commsite2\", WebTemplate: \"STS#3\", });","title":"Create from Props"},{"location":"v2/sp/sites/#create-a-modern-team-site","text":"Note: Works only in SharePoint online. It wont work with App only tokens Creates a modern team site backed by O365 group. Property Type Required Description displayName string yes The title/displayName of the site to be created. alias string yes Alias of the underlying Office 365 Group. isPublic boolean yes Defines whether the Office 365 Group will be public (default), or private. lcid number yes The language to use for the site. If not specified will default to English (1033). description string no The description of the modern team site. classification string no The Site classification to use. For instance \"Contoso Classified\". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information owners string array (string[]) no The Owners of the site to be created hubSiteId string no The Guid of the already existing Hub site siteDesignId string no The Guid of the site design to be used. You can use the below default OOTB GUIDs: Topic: null Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767 Blank: f6cc5403-0d63-442e-96c0-285923709ffc import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; const result = await sp.site.createModernTeamSite( \"displayName\", \"alias\", true, 1033, \"description\", \"HBI\", [\"user1@tenant.onmicrosoft.com\",\"user2@tenant.onmicrosoft.com\",\"user3@tenant.onmicrosoft.com\"], \"a00ec589-ea9f-4dba-a34e-67e78d41e509\", \"f6cc5403-0d63-442e-96c0-285923709ffc\" ); console.log(d);","title":"Create a modern team site"},{"location":"v2/sp/sites/#create-from-props_1","text":"You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/sites\"; // in this case you supply a single struct deinfing the creation props const result = await sp.site.createModernTeamSiteFromProps({ alias: \"JenniferGarner\", displayName: \"A Test Site\", owners: [\"patrick@three18studios.com\"], });","title":"Create from Props"},{"location":"v2/sp/sites/#delete-a-site-collection","text":"Using the library, you can delete a specific site collection import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sites\"; import { Site } from \"@pnp/sp/sites\"; // Delete the current site await sp.site.delete(); // Specify which site to delete const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const site2 = Site(siteUrl); await site2.delete();","title":"Delete a site collection"},{"location":"v2/sp/sites/#check-if-a-site-collection-exists","text":"Using the library, you can check if a specific site collection exist or not on your tenant import { sp } from \"@pnp/sp\"; // Specify which site to verify const siteUrl = \"https://tenant.sharepoint.com/sites/subsite\"; const exists = sp.site.exists(siteUrl); console.log(exists);","title":"Check if a Site Collection Exists"},{"location":"v2/sp/social/","text":"@pnp/sp/ - social \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions. getFollowedSitesUri \u00b6 Gets a URI to a site that lists the current user's followed sites. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedSitesUri(); getFollowedDocumentsUri \u00b6 Gets a URI to a site that lists the current user's followed documents. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedDocumentsUri(); follow \u00b6 Makes the current user start following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // follow a site const r1 = await sp.social.follow({ ActorType: SocialActorType.Site, ContentUri: \"htts://tenant.sharepoint.com/sites/site\", }); // follow a person const r2 = await sp.social.follow({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); // follow a doc const r3 = await sp.social.follow({ ActorType: SocialActorType.Document, ContentUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\", }); // follow a tag // You need the tag GUID to start following a tag. // You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. // See How to get a tag's GUID based on the tag's name by using the JavaScript object model. // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid const r4 = await sp.social.follow({ ActorType: SocialActorType.Tag, TagGuid: \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\", }); isFollowed \u00b6 Indicates whether the current user is following a specified user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.isFollowed({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); stopFollowing \u00b6 Makes the current user stop following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.stopFollowing({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); my \u00b6 get \u00b6 Gets this user's social information import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const r = await sp.social.my(); followed \u00b6 Gets users, documents, sites, and tags that the current user is following based on the supplied flags. import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get all the followed documents const r1 = await sp.social.my.followed(SocialActorTypes.Document); // get all the followed documents and sites const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site); // get all the followed sites updated in the last 24 hours const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours); followedCount \u00b6 Works as followed but returns on the count of actors specified by the query import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followedCount(SocialActorTypes.Document); followers \u00b6 Gets the users who are following the current user. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followers(); suggestions \u00b6 Gets users who the current user might want to follow. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.suggestions();","title":"@pnp/sp/ - social"},{"location":"v2/sp/social/#pnpsp-social","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not with app-only permissions.","title":"@pnp/sp/ - social"},{"location":"v2/sp/social/#getfollowedsitesuri","text":"Gets a URI to a site that lists the current user's followed sites. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedSitesUri();","title":"getFollowedSitesUri"},{"location":"v2/sp/social/#getfolloweddocumentsuri","text":"Gets a URI to a site that lists the current user's followed documents. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const uri = await sp.social.getFollowedDocumentsUri();","title":"getFollowedDocumentsUri"},{"location":"v2/sp/social/#follow","text":"Makes the current user start following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // follow a site const r1 = await sp.social.follow({ ActorType: SocialActorType.Site, ContentUri: \"htts://tenant.sharepoint.com/sites/site\", }); // follow a person const r2 = await sp.social.follow({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, }); // follow a doc const r3 = await sp.social.follow({ ActorType: SocialActorType.Document, ContentUri: \"https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx\", }); // follow a tag // You need the tag GUID to start following a tag. // You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model. // See How to get a tag's GUID based on the tag's name by using the JavaScript object model. // https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid const r4 = await sp.social.follow({ ActorType: SocialActorType.Tag, TagGuid: \"19a4a484-c1dc-4bc5-8c93-bb96245ce928\", });","title":"follow"},{"location":"v2/sp/social/#isfollowed","text":"Indicates whether the current user is following a specified user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.isFollowed({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, });","title":"isFollowed"},{"location":"v2/sp/social/#stopfollowing","text":"Makes the current user stop following a user, document, site, or tag import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // pass the same social actor struct as shown in follow example for each type const r = await sp.social.stopFollowing({ AccountName: \"i:0#.f|membership|person@tenant.com\", ActorType: SocialActorType.User, });","title":"stopFollowing"},{"location":"v2/sp/social/#my","text":"","title":"my"},{"location":"v2/sp/social/#get","text":"Gets this user's social information import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; const r = await sp.social.my();","title":"get"},{"location":"v2/sp/social/#followed","text":"Gets users, documents, sites, and tags that the current user is following based on the supplied flags. import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get all the followed documents const r1 = await sp.social.my.followed(SocialActorTypes.Document); // get all the followed documents and sites const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site); // get all the followed sites updated in the last 24 hours const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);","title":"followed"},{"location":"v2/sp/social/#followedcount","text":"Works as followed but returns on the count of actors specified by the query import { sp } from \"@pnp/sp\"; import { SocialActorType } from \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followedCount(SocialActorTypes.Document);","title":"followedCount"},{"location":"v2/sp/social/#followers","text":"Gets the users who are following the current user. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.followers();","title":"followers"},{"location":"v2/sp/social/#suggestions","text":"Gets users who the current user might want to follow. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/social\"; // get the followed documents count const r = await sp.social.my.suggestions();","title":"suggestions"},{"location":"v2/sp/sp-utilities-utility/","text":"@pnp/sp/utilities \u00b6 Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching. sendEmail \u00b6 This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below). EmailProperties \u00b6 export interface EmailProperties { To: string[]; CC?: string[]; BCC?: string[]; Subject: string; Body: string; AdditionalHeaders?: TypedHash; From?: string; } Usage \u00b6 You must define the To, Subject, and Body values - the remaining are optional. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { IEmailProperties } from \"@pnp/sp/sputilities\"; const emailProps: IEmailProperties = { To: [\"user@site.com\"], CC: [\"user2@site.com\", \"user3@site.com\"], BCC: [\"user4@site.com\", \"user5@site.com\"], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" } }; await sp.utility.sendEmail(emailProps); console.log(\"Email Sent!\"); getCurrentUserEmailAddresses \u00b6 This method returns the current user's email addresses known to SharePoint. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let addressString: string = await sp.utility.getCurrentUserEmailAddresses(); // and use it with sendEmail await sp.utility.sendEmail({ To: [addressString], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" }, }); resolvePrincipal \u00b6 Gets information about a principal that matches the specified Search criteria import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principal : IPrincipalInfo = await sp.utility.resolvePrincipal(\"user@site.com\", PrincipalType.User, PrincipalSource.All, true, false, true); console.log(principal); searchPrincipals \u00b6 Gets information about the principals that match the specified Search criteria. import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals(\"john\", PrincipalType.User, PrincipalSource.All,\"\", 10); console.log(principals); createEmailBodyForInvitation \u00b6 Gets the external (outside the firewall) URL to a document or resource in a site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let url : string = await sp.utility.createEmailBodyForInvitation(\"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\"); console.log(url); expandGroupsToPrincipals \u00b6 Resolves the principals contained within the supplied groups import { sp, IPrincipalInfo } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"]); console.log(principals); // optionally supply a max results count. Default is 30. let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"], 10); console.log(principals); createWikiPage \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { ICreateWikiPageResult } from \"@pnp/sp/sputilities\"; let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({ ServerRelativeUrl: \"/sites/dev/SitePages/mynewpage.aspx\", WikiHtmlContent: \"This is my page content. It supports rich html.\", }); // newPage contains the raw data returned by the service console.log(newPage.data); // newPage contains a File instance you can use to further update the new page let file = await newPage.file(); console.log(file);","title":"@pnp/sp/utilities"},{"location":"v2/sp/sp-utilities-utility/#pnpsputilities","text":"Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.","title":"@pnp/sp/utilities"},{"location":"v2/sp/sp-utilities-utility/#sendemail","text":"This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).","title":"sendEmail"},{"location":"v2/sp/sp-utilities-utility/#emailproperties","text":"export interface EmailProperties { To: string[]; CC?: string[]; BCC?: string[]; Subject: string; Body: string; AdditionalHeaders?: TypedHash; From?: string; }","title":"EmailProperties"},{"location":"v2/sp/sp-utilities-utility/#usage","text":"You must define the To, Subject, and Body values - the remaining are optional. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { IEmailProperties } from \"@pnp/sp/sputilities\"; const emailProps: IEmailProperties = { To: [\"user@site.com\"], CC: [\"user2@site.com\", \"user3@site.com\"], BCC: [\"user4@site.com\", \"user5@site.com\"], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" } }; await sp.utility.sendEmail(emailProps); console.log(\"Email Sent!\");","title":"Usage"},{"location":"v2/sp/sp-utilities-utility/#getcurrentuseremailaddresses","text":"This method returns the current user's email addresses known to SharePoint. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let addressString: string = await sp.utility.getCurrentUserEmailAddresses(); // and use it with sendEmail await sp.utility.sendEmail({ To: [addressString], Subject: \"This email is about...\", Body: \"Here is the body. It supports html\", AdditionalHeaders: { \"content-type\": \"text/html\" }, });","title":"getCurrentUserEmailAddresses"},{"location":"v2/sp/sp-utilities-utility/#resolveprincipal","text":"Gets information about a principal that matches the specified Search criteria import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principal : IPrincipalInfo = await sp.utility.resolvePrincipal(\"user@site.com\", PrincipalType.User, PrincipalSource.All, true, false, true); console.log(principal);","title":"resolvePrincipal"},{"location":"v2/sp/sp-utilities-utility/#searchprincipals","text":"Gets information about the principals that match the specified Search criteria. import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals(\"john\", PrincipalType.User, PrincipalSource.All,\"\", 10); console.log(principals);","title":"searchPrincipals"},{"location":"v2/sp/sp-utilities-utility/#createemailbodyforinvitation","text":"Gets the external (outside the firewall) URL to a document or resource in a site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let url : string = await sp.utility.createEmailBodyForInvitation(\"https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx\"); console.log(url);","title":"createEmailBodyForInvitation"},{"location":"v2/sp/sp-utilities-utility/#expandgroupstoprincipals","text":"Resolves the principals contained within the supplied groups import { sp, IPrincipalInfo } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"]); console.log(principals); // optionally supply a max results count. Default is 30. let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals([\"Dev Owners\", \"Dev Members\"], 10); console.log(principals);","title":"expandGroupsToPrincipals"},{"location":"v2/sp/sp-utilities-utility/#createwikipage","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/sputilities\"; import { ICreateWikiPageResult } from \"@pnp/sp/sputilities\"; let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({ ServerRelativeUrl: \"/sites/dev/SitePages/mynewpage.aspx\", WikiHtmlContent: \"This is my page content. It supports rich html.\", }); // newPage contains the raw data returned by the service console.log(newPage.data); // newPage contains a File instance you can use to further update the new page let file = await newPage.file(); console.log(file);","title":"createWikiPage"},{"location":"v2/sp/subscriptions/","text":"@pnp/sp/subscriptions \u00b6 Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library. ISubscriptions \u00b6 Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import {sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/presets/all\"; Add a webhook \u00b6 Using this library, you can add a webhook to a specified list within the SharePoint site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\"; // This is the URL which will be called by SharePoint when there is a change in the list const notificationUrl = \"\"; // Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date. const expiryDate = dateAdd(new Date(), \"day\" , 180).toISOString(); // Adds a webhook to the Documents library var res = await sp.web.lists.getByTitle(\"Documents\").subscriptions.add(notificationUrl,expiryDate); Get all webhooks added to a list \u00b6 Read all the webhooks' details which are associated to the list import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; const res = await sp.web.lists.getByTitle(\"Documents\").subscriptions(); ISubscription \u00b6 This interface provides the methods for managing a particular webhook. Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from \"@pnp/sp/presets/all\"; Managing a webhook \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; // Get details of a webhook based on its ID const webhookId = \"1f029e5c-16e4-4941-b46f-67895118763f\"; const webhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId)(); // Update a webhook const newDate = dateAdd(new Date(), \"day\" , 150).toISOString(); const updatedWebhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).update(newDate); // Delete a webhook await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).delete();","title":"@pnp/sp/subscriptions"},{"location":"v2/sp/subscriptions/#pnpspsubscriptions","text":"Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.","title":"@pnp/sp/subscriptions"},{"location":"v2/sp/subscriptions/#isubscriptions","text":"Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import {sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/presets/all\";","title":"ISubscriptions"},{"location":"v2/sp/subscriptions/#add-a-webhook","text":"Using this library, you can add a webhook to a specified list within the SharePoint site. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\"; // This is the URL which will be called by SharePoint when there is a change in the list const notificationUrl = \"\"; // Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date. const expiryDate = dateAdd(new Date(), \"day\" , 180).toISOString(); // Adds a webhook to the Documents library var res = await sp.web.lists.getByTitle(\"Documents\").subscriptions.add(notificationUrl,expiryDate);","title":"Add a webhook"},{"location":"v2/sp/subscriptions/#get-all-webhooks-added-to-a-list","text":"Read all the webhooks' details which are associated to the list import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; const res = await sp.web.lists.getByTitle(\"Documents\").subscriptions();","title":"Get all webhooks added to a list"},{"location":"v2/sp/subscriptions/#isubscription","text":"This interface provides the methods for managing a particular webhook. Scenario Import Statement Selective import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import { Subscriptions, ISubscriptions, Subscription, ISubscription} from \"@pnp/sp/subscriptions\"; import \"@pnp/sp/subscriptions/list\" Preset: All import { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from \"@pnp/sp/presets/all\";","title":"ISubscription"},{"location":"v2/sp/subscriptions/#managing-a-webhook","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/subscriptions\"; // Get details of a webhook based on its ID const webhookId = \"1f029e5c-16e4-4941-b46f-67895118763f\"; const webhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId)(); // Update a webhook const newDate = dateAdd(new Date(), \"day\" , 150).toISOString(); const updatedWebhook = await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).update(newDate); // Delete a webhook await sp.web.lists.getByTitle(\"Documents\").subscriptions.getById(webhookId).delete();","title":"Managing a webhook"},{"location":"v2/sp/taxonomy/","text":"@pnp/sp/taxonomy \u00b6 Provides access to the v2.1 api term store Docs updated with v2.0.9 release as the underlying API changed. \u00b6 NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabalize this note will be removed. Term Store \u00b6 Access term store data from the root sp object as shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermStoreInfo } from \"@pnp/sp/taxonomy\"; // get term store data const info: ITermStoreInfo = await sp.termStore(); Term Groups \u00b6 Access term group information List \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups const info: ITermGroupInfo[] = await sp.termStore.groups(); Get By Id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups data const info: ITermGroupInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\")(); Term Sets \u00b6 Access term set information List \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get get set info const info: ITermSetInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets(); Get By Id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermSetInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")(); getAllChildrenAsOrderedTree \u00b6 Added in 2.0.13 This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; import { dateAdd, PnPClientStorage } from \"@pnp/core\"; // here we get all the children of a given set const childTree = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); // here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available const store = new PnPClientStorage(); // our tree likely doesn't change much in 30 minutes for most applications // adjust to be longer or shorter as needed const cachedTree = await store.local.getOrPut(\"myKey\", () => { return sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); }, dateAdd(new Date(), \"minute\", 30)); Terms \u00b6 Access term set information List \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children(); List (terms) \u00b6 Added in 2.0.13 You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms(); Get By Id \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\")(); Get Term Parent \u00b6 Behavior Change in 2.1.0 The server API changed again, resulting in the removal of the \"parent\" property from ITerm as it is not longer supported as a path property. You now must use \"expand\" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; // get a ref to the set const set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\"); // get a term's information and expand parent to get the parent info as well const w = await set.getTermById(\"338666a8-1111-2222-3333-f72471314e72\").expand(\"parent\")(); // get a ref to the parent term const parent = set.getTermById(w.parent.id); // make a request for the parent term's info - this data currently match the results in the expand call above, but this // is to demonstrate how to gain a ref to the parent and select its data const parentInfo = await parent.select(\"Id\", \"Descriptions\")();","title":"@pnp/sp/taxonomy"},{"location":"v2/sp/taxonomy/#pnpsptaxonomy","text":"Provides access to the v2.1 api term store","title":"@pnp/sp/taxonomy"},{"location":"v2/sp/taxonomy/#docs-updated-with-v209-release-as-the-underlying-api-changed","text":"NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabalize this note will be removed.","title":"Docs updated with v2.0.9 release as the underlying API changed."},{"location":"v2/sp/taxonomy/#term-store","text":"Access term store data from the root sp object as shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermStoreInfo } from \"@pnp/sp/taxonomy\"; // get term store data const info: ITermStoreInfo = await sp.termStore();","title":"Term Store"},{"location":"v2/sp/taxonomy/#term-groups","text":"Access term group information","title":"Term Groups"},{"location":"v2/sp/taxonomy/#list","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups const info: ITermGroupInfo[] = await sp.termStore.groups();","title":"List"},{"location":"v2/sp/taxonomy/#get-by-id","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermGroupInfo } from \"@pnp/sp/taxonomy\"; // get term groups data const info: ITermGroupInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\")();","title":"Get By Id"},{"location":"v2/sp/taxonomy/#term-sets","text":"Access term set information","title":"Term Sets"},{"location":"v2/sp/taxonomy/#list_1","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get get set info const info: ITermSetInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets();","title":"List"},{"location":"v2/sp/taxonomy/#get-by-id_1","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermSetInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermSetInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\")();","title":"Get By Id"},{"location":"v2/sp/taxonomy/#getallchildrenasorderedtree","text":"Added in 2.0.13 This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; import { dateAdd, PnPClientStorage } from \"@pnp/core\"; // here we get all the children of a given set const childTree = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); // here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available const store = new PnPClientStorage(); // our tree likely doesn't change much in 30 minutes for most applications // adjust to be longer or shorter as needed const cachedTree = await store.local.getOrPut(\"myKey\", () => { return sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getAllChildrenAsOrderedTree(); }, dateAdd(new Date(), \"minute\", 30));","title":"getAllChildrenAsOrderedTree"},{"location":"v2/sp/taxonomy/#terms","text":"Access term set information","title":"Terms"},{"location":"v2/sp/taxonomy/#list_2","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").children();","title":"List"},{"location":"v2/sp/taxonomy/#list-terms","text":"Added in 2.0.13 You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // list all the terms that are direct children of this set const infos: ITermInfo[] = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").terms();","title":"List (terms)"},{"location":"v2/sp/taxonomy/#get-by-id_2","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; import { ITermInfo } from \"@pnp/sp/taxonomy\"; // get term set data const info: ITermInfo = await sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\").getTermById(\"338666a8-1111-2222-3333-f72471314e72\")();","title":"Get By Id"},{"location":"v2/sp/taxonomy/#get-term-parent","text":"Behavior Change in 2.1.0 The server API changed again, resulting in the removal of the \"parent\" property from ITerm as it is not longer supported as a path property. You now must use \"expand\" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/taxonomy\"; // get a ref to the set const set = sp.termStore.groups.getById(\"338666a8-1111-2222-3333-f72471314e72\").sets.getById(\"338666a8-1111-2222-3333-f72471314e72\"); // get a term's information and expand parent to get the parent info as well const w = await set.getTermById(\"338666a8-1111-2222-3333-f72471314e72\").expand(\"parent\")(); // get a ref to the parent term const parent = set.getTermById(w.parent.id); // make a request for the parent term's info - this data currently match the results in the expand call above, but this // is to demonstrate how to gain a ref to the parent and select its data const parentInfo = await parent.select(\"Id\", \"Descriptions\")();","title":"Get Term Parent"},{"location":"v2/sp/tenant-properties/","text":"@pnp/sp/web - tenant properties \u00b6 You can set, read, and remove tenant properties using the methods shown below: setStorageEntity \u00b6 This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); // specify required key and value await w.setStorageEntity(\"Test1\", \"Value 1\"); // specify optional description and comments await w.setStorageEntity(\"Test2\", \"Value 2\", \"description\", \"comments\"); getStorageEntity \u00b6 This method can be used from any web to retrieve values previously set. import { sp, IStorageEntity } from \"@pnp/sp/presets/all\"; const prop: IStorageEntity = await sp.web.getStorageEntity(\"Test1\"); console.log(prop.Value); removeStorageEntity \u00b6 This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); await w.removeStorageEntity(\"Test1\");","title":"@pnp/sp/web - tenant properties"},{"location":"v2/sp/tenant-properties/#pnpspweb-tenant-properties","text":"You can set, read, and remove tenant properties using the methods shown below:","title":"@pnp/sp/web - tenant properties"},{"location":"v2/sp/tenant-properties/#setstorageentity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); // specify required key and value await w.setStorageEntity(\"Test1\", \"Value 1\"); // specify optional description and comments await w.setStorageEntity(\"Test2\", \"Value 2\", \"description\", \"comments\");","title":"setStorageEntity"},{"location":"v2/sp/tenant-properties/#getstorageentity","text":"This method can be used from any web to retrieve values previously set. import { sp, IStorageEntity } from \"@pnp/sp/presets/all\"; const prop: IStorageEntity = await sp.web.getStorageEntity(\"Test1\"); console.log(prop.Value);","title":"getStorageEntity"},{"location":"v2/sp/tenant-properties/#removestorageentity","text":"This method MUST be called in the context of the app catalog web or you will get an access denied message. import { Web } from \"@pnp/sp/webs\"; const w = Web(\"https://tenant.sharepoint.com/sites/appcatalog/\"); await w.removeStorageEntity(\"Test1\");","title":"removeStorageEntity"},{"location":"v2/sp/user-custom-actions/","text":"@pnp/sp/user-custom-actions \u00b6 Represents a custom action associated with a SharePoint list, web or site collection. IUserCustomActions \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IUserCustomActions, IUserCustomAction } from \"@pnp/sp/user-custom-actions\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/user-custom-actions\"; Preset: All import { sp, IUserCustomActions, IUserCustomAction } from \"@pnp/sp/presents/all\"; Get a collection of User Custom Actions from a web \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const userCustomActions = sp.web.userCustomActions(); Add a new User Custom Action \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions'; const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"Location\": \"ScriptLink\", \"ScriptSrc\": \"https://...\" }; const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues); Get a User Custom Action by ID \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const uca: IUserCustomAction = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const ucaData = await uca(); Clear the User Custom Action collection \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; // Site collection level await sp.site.userCustomActions.clear(); // Site (web) level await sp.web.userCustomActions.clear(); // List level await sp.web.lists.getByTitle(\"Documents\").userCustomActions.clear(); IUserCustomAction \u00b6 Update an existing User Custom Action \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions'; const uca = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"ScriptSrc\": \"https://...\" }; const response: IUserCustomActionUpdateResult = uca.update(newValues);","title":"@pnp/sp/user-custom-actions"},{"location":"v2/sp/user-custom-actions/#pnpspuser-custom-actions","text":"Represents a custom action associated with a SharePoint list, web or site collection.","title":"@pnp/sp/user-custom-actions"},{"location":"v2/sp/user-custom-actions/#iusercustomactions","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { IUserCustomActions, IUserCustomAction } from \"@pnp/sp/user-custom-actions\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/user-custom-actions\"; Preset: All import { sp, IUserCustomActions, IUserCustomAction } from \"@pnp/sp/presents/all\";","title":"IUserCustomActions"},{"location":"v2/sp/user-custom-actions/#get-a-collection-of-user-custom-actions-from-a-web","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const userCustomActions = sp.web.userCustomActions();","title":"Get a collection of User Custom Actions from a web"},{"location":"v2/sp/user-custom-actions/#add-a-new-user-custom-action","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions'; const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"Location\": \"ScriptLink\", \"ScriptSrc\": \"https://...\" }; const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);","title":"Add a new User Custom Action"},{"location":"v2/sp/user-custom-actions/#get-a-user-custom-action-by-id","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; const uca: IUserCustomAction = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const ucaData = await uca();","title":"Get a User Custom Action by ID"},{"location":"v2/sp/user-custom-actions/#clear-the-user-custom-action-collection","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; // Site collection level await sp.site.userCustomActions.clear(); // Site (web) level await sp.web.userCustomActions.clear(); // List level await sp.web.lists.getByTitle(\"Documents\").userCustomActions.clear();","title":"Clear the User Custom Action collection"},{"location":"v2/sp/user-custom-actions/#iusercustomaction","text":"","title":"IUserCustomAction"},{"location":"v2/sp/user-custom-actions/#update-an-existing-user-custom-action","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/user-custom-actions\"; import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions'; const uca = sp.web.userCustomActions.getById(\"00000000-0000-0000-0000-000000000000\"); const newValues: TypedHash = { \"Title\": \"New Title\", \"Description\": \"New Description\", \"ScriptSrc\": \"https://...\" }; const response: IUserCustomActionUpdateResult = uca.update(newValues);","title":"Update an existing User Custom Action"},{"location":"v2/sp/views/","text":"@pnp/sp/views \u00b6 Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view. IViews \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Views, IViews } from \"@pnp/sp/views\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/views\"; Preset: All import { sp, Views, IViews } from \"@pnp/sp/presets/all\"; Get views in a list \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // get all the views and their properties const views1 = await list.views(); // you can use odata select operations to get just a set a fields const views2 = await list.views.select(\"Id\", \"Title\")(); // get the top three views const views3 = await list.views.top(3)(); Add a View \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // create a new view with default fields and properties const result = await list.views.add(\"My New View\"); // create a new view with specific properties const result2 = await list.views.add(\"My New View 2\", false, { RowLimit: 10, ViewQuery: \"\", }); // manipulate the view's fields await result2.view.fields.removeAll(); await Promise.all([ result2.view.fields.add(\"Title\"), result2.view.fields.add(\"Modified\"), ]); IView \u00b6 Get a View's Information \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\")(); const result2 = await list.views.getByTitle(\"My View\")(); const result3 = await list.views.getByTitle(\"My View\").select(\"Id\", \"Title\")(); const result4 = await list.defaultView(); const result5 = await list.getView(\"{GUID view id}\")(); fields \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").fields(); update \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").update({ RowLimit: 20, }); renderAsHtml \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const result = await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").renderAsHtml(); setViewXml \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").setViewXml(viewXml); delete \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").delete(); ViewFields \u00b6 getSchemaXml \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const xml = await sp.web.lists.getByTitle(\"My List\").defaultView.fields.getSchemaXml(); add \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.add(\"Created\"); move \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.move(\"Created\", 0); remove \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.remove(\"Created\"); removeAll \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.removeAll();","title":"@pnp/sp/views"},{"location":"v2/sp/views/#pnpspviews","text":"Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.","title":"@pnp/sp/views"},{"location":"v2/sp/views/#iviews","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Views, IViews } from \"@pnp/sp/views\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/views\"; Preset: All import { sp, Views, IViews } from \"@pnp/sp/presets/all\";","title":"IViews"},{"location":"v2/sp/views/#get-views-in-a-list","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // get all the views and their properties const views1 = await list.views(); // you can use odata select operations to get just a set a fields const views2 = await list.views.select(\"Id\", \"Title\")(); // get the top three views const views3 = await list.views.top(3)();","title":"Get views in a list"},{"location":"v2/sp/views/#add-a-view","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); // create a new view with default fields and properties const result = await list.views.add(\"My New View\"); // create a new view with specific properties const result2 = await list.views.add(\"My New View 2\", false, { RowLimit: 10, ViewQuery: \"\", }); // manipulate the view's fields await result2.view.fields.removeAll(); await Promise.all([ result2.view.fields.add(\"Title\"), result2.view.fields.add(\"Modified\"), ]);","title":"Add a View"},{"location":"v2/sp/views/#iview","text":"","title":"IView"},{"location":"v2/sp/views/#get-a-views-information","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\")(); const result2 = await list.views.getByTitle(\"My View\")(); const result3 = await list.views.getByTitle(\"My View\").select(\"Id\", \"Title\")(); const result4 = await list.defaultView(); const result5 = await list.getView(\"{GUID view id}\")();","title":"Get a View's Information"},{"location":"v2/sp/views/#fields","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").fields();","title":"fields"},{"location":"v2/sp/views/#update","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const list = sp.web.lists.getByTitle(\"My List\"); const result = await list.views.getById(\"{GUID view id}\").update({ RowLimit: 20, });","title":"update"},{"location":"v2/sp/views/#renderashtml","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const result = await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").renderAsHtml();","title":"renderAsHtml"},{"location":"v2/sp/views/#setviewxml","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").setViewXml(viewXml);","title":"setViewXml"},{"location":"v2/sp/views/#delete","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const viewXml = \"...\"; await sp.web.lists.getByTitle(\"My List\").views.getById(\"{GUID view id}\").delete();","title":"delete"},{"location":"v2/sp/views/#viewfields","text":"","title":"ViewFields"},{"location":"v2/sp/views/#getschemaxml","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; const xml = await sp.web.lists.getByTitle(\"My List\").defaultView.fields.getSchemaXml();","title":"getSchemaXml"},{"location":"v2/sp/views/#add","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.add(\"Created\");","title":"add"},{"location":"v2/sp/views/#move","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.move(\"Created\", 0);","title":"move"},{"location":"v2/sp/views/#remove","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.remove(\"Created\");","title":"remove"},{"location":"v2/sp/views/#removeall","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/lists\"; import \"@pnp/sp/views\"; await sp.web.lists.getByTitle(\"My List\").defaultView.fields.removeAll();","title":"removeAll"},{"location":"v2/sp/webs/","text":"@pnp/sp/webs \u00b6 Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types. IWebs \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Webs, IWebs } from \"@pnp/sp/presets/core\"; Add Web \u00b6 Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions. import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; const result = await sp.web.webs.add(\"title\", \"subweb1\"); // show the response from the server when adding the web console.log(result.data); // we can immediately operate on the new web result.web.select(\"Title\")().then((w: IWebAddResult) => { // show our title console.log(w.Title); }); import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; // create a German language wiki site with title, url, description, which does not inherit permissions sp.web.webs.add(\"wiki\", \"subweb2\", \"a wiki web\", \"WIKI#0\", 1031, false).then((w: IWebAddResult) => { // ... }); IWeb \u00b6 Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Web, IWeb } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Web, IWeb } from \"@pnp/sp/presets/core\"; Access a Web \u00b6 There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named \"web\" which represents an IWeb instance - regardless of how it was initially accessed. Access the web from the imported \"sp\" object using selective import: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'all' preset import { sp } from \"@pnp/sp/presets/all\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'core' preset import { sp } from \"@pnp/sp/presets/core\"; const r = await sp.web(); Create a web instance using the factory function import { Web } from \"@pnp/sp/webs\"; const web = Web(\"https://something.sharepoint.com/sites/dev\"); const r = await web(); webs \u00b6 Access the child webs collection of this web const webs = web.webs(); Get A Web's properties \u00b6 // basic get of the webs properties const props = await web(); // use odata operators to get specific fields const props2 = await web.select(\"Title\")(); // type the result to match what you are requesting const props3 = await web.select(\"Title\")<{ Title: string }>(); getParentWeb \u00b6 Get the data and IWeb instance for the parent web for the given web instance import { IOpenWebByIdResult } from \"@pnp/sp/sites\"; const web: IOpenWebByIdResult = web.getParentWeb(); getSubwebsFilteredForCurrentUser \u00b6 Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member. const subWebs = await web.getSubwebsFilteredForCurrentUser()(); // apply odata operations to the collection const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select(\"Title\", \"Language\").orderBy(\"Created\", true)(); Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo. allProperties \u00b6 Allows access to the web's all properties collection. This is readonly in REST. const props = await web.allProperties(); // select certain props const props2 = await web.allProperties.select(\"prop1\", \"prop2\")(); webinfos \u00b6 Gets a collection of WebInfos for this web's subwebs const infos = await web.webinfos(); // or select certain fields const infos2 = await web.webinfos.select(\"Title\", \"Description\")(); // or filter const infos3 = await web.webinfos.filter(\"Title eq 'MyWebTitle'\")(); // or both const infos4 = await web.webinfos.select(\"Title\", \"Description\").filter(\"Title eq 'MyWebTitle'\")(); // get the top 4 ordered by Title const infos5 = await web.webinfos.top(4).orderBy(\"Title\")(); Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo. update \u00b6 Updates this web instance with the supplied properties // update the web's title and description const result = await web.update({ Title: \"New Title\", Description: \"My new description\", }); // a project implementation could wrap the update to provide type information for your expected fields: import { IWebUpdateResult } from \"@pnp/sp/webs\"; interface IWebUpdateProps { Title: string; Description: string; } function updateWeb(props: IWebUpdateProps): Promise { web.update(props); } Delete a Web \u00b6 await web.delete(); applyTheme \u00b6 Applies the theme specified by the contents of each of the files specified in the arguments to the site import { combine } from \"@pnp/core\"; // we are going to apply the theme to this sub web as an example const web = Web(\"https://{tenant}.sharepoint.com/sites/dev/subweb\"); // the urls to the color and font need to both be from the catalog at the root // these urls can be constants or calculated from existing urls const colorUrl = combine(\"/\", \"sites/dev\", \"_catalogs/theme/15/palette011.spcolor\"); // this gives us the same result const fontUrl = \"/sites/dev/_catalogs/theme/15/fontscheme007.spfont\"; // apply the font and color, no background image, and don't share this theme await web.applyTheme(colorUrl, fontUrl, \"\", false); applyWebTemplate & availableWebTemplates \u00b6 Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios. const templates = (await web.availableWebTemplates().select(\"Name\")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name)); // apply the wiki template const template = templates.length > 0 ? templates[0].Name : \"STS#0\"; await web.applyWebTemplate(template); getChanges \u00b6 Returns the collection of changes from the change log that have occurred within the web, based on the specified query. // get the web changes including add, update, and delete const changes = await web.getChanges({ Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Update: true, Web: true, }); mapToIcon \u00b6 Returns the name of the image file for the icon that is used to represent the specified file import { combine } from \"@pnp/core\"; const iconFileName = await web.mapToIcon(\"test.docx\"); // iconPath === \"icdocx.png\" // which you can need to map to a real url const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`; // OR dynamically const webData = await sp.web.select(\"Url\")(); const iconFullPath2 = combine(webData.Url, \"_layouts\", \"images\", iconFileName); // OR within SPFx using the context const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, \"_layouts\", \"images\", iconFileName); // You can also set size // 16x16 pixels = 0, 32x32 pixels = 1 const icon32FileName = await web.mapToIcon(\"test.docx\", 1); storage entities \u00b6 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import { IStorageEntity } from \"@pnp/sp/webs\"; // needs to be unique, GUIDs are great const key = \"my-storage-key\"; // read an existing entity const entity: IStorageEntity = await web.getStorageEntity(key); // setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site // you can get the tenant app catalog using the getTenantAppCatalogWeb const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb(); tenantAppCatalogWeb.setStorageEntity(key, \"new value\"); // set other properties tenantAppCatalogWeb.setStorageEntity(key, \"another value\", \"description\", \"comments\"); const entity2: IStorageEntity = await web.getStorageEntity(key); /* entity2 === { Value: \"another value\", Comment: \"comments\"; Description: \"description\", }; */ // you can also remove a storage entity await tenantAppCatalogWeb.removeStorageEntity(key); appcatalog imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/appcatalog\"; Selective 2 import \"@pnp/sp/appcatalog/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; getAppCatalog \u00b6 Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url. import { IApp } from \"@pnp/sp/appcatalog\"; const appWeb = web.getAppCatalog(); // appWeb url === web url const app: IApp = appWeb.getAppById(\"{your app id}\"); const appWeb2 = web.getAppCatalog(\"https://tenant.sharepoing.com/sites/someappcatalog\"); // appWeb2 url === \"https://tenant.sharepoing.com/sites/someappcatalog\" client-side-pages imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/client-side-pages\"; Selective 2 import \"@pnp/sp/client-side-pages/web\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // simplest add a page example const page = await sp.web.addClientsidePage(\"mypage1\"); // simplest load a page example const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\"); content-type imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; contentTypes \u00b6 Allows access to the collection of content types in this web. const cts = await web.contentTypes(); // you can also select fields and use other odata operators const cts2 = await web.contentTypes.select(\"Name\")(); features imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/features\"; Selective 2 import \"@pnp/sp/features/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; features \u00b6 Allows access to the collection of content types in this web. const features = await web.features(); fields imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; fields \u00b6 Allows access to the collection of fields in this web. const fields = await web.fields(); files imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/files\"; Selective 2 import \"@pnp/sp/files/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; getFileByServerRelativeUrl \u00b6 Gets a file by server relative url import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativeUrl(\"/sites/dev/library/myfile.docx\"); getFileByServerRelativePath \u00b6 Gets a file by server relative url if your file name contains # and % characters import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativePath(\"/sites/dev/library/my # file%.docx\"); folders imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; folders \u00b6 Gets the collection of folders in this web const folders = await web.folders(); // you can also filter and select as with any collection const folders2 = await web.folders.select(\"ServerRelativeUrl\", \"TimeLastModified\").filter(\"ItemCount gt 0\")(); // or get the most recently modified folder const folders2 = await web.folders.orderBy(\"TimeLastModified\").top(1)(); rootFolder \u00b6 Gets the root folder of the web const folder = await web.rootFolder(); getFolderByServerRelativeUrl \u00b6 Gets a folder by server relative url import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativeUrl(\"/sites/dev/library\"); getFolderByServerRelativePath \u00b6 Gets a folder by server relative url if your folder name contains # and % characters import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativePath(\"/sites/dev/library/my # folder%/\"); hubsites imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/hubsites\"; Selective 2 import \"@pnp/sp/hubsites/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; hubSiteData \u00b6 Gets hub site data for the current web import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; // get the data and force a refresh const data: IHubSiteWebData = await web.hubSiteData(true); syncHubSiteTheme \u00b6 Applies theme updates from the parent hub site collection await web.syncHubSiteTheme(); lists imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/lists\"; Selective 2 import \"@pnp/sp/lists/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\"; lists \u00b6 Gets the collection of all lists that are contained in the Web site import { ILists } from \"@pnp/sp/lists\"; const lists: ILists = web.lists; // you can always order the lists and select properties const data = await lists.select(\"Title\").orderBy(\"Title\")(); // and use other odata operators as well const data2 = await web.lists.top(3).orderBy(\"LastItemModifiedDate\")(); siteUserInfoList \u00b6 Gets the UserInfo list of the site collection that contains the Web site import { IList } from \"@pnp/sp/lists\"; const list: IList = web.siteUserInfoList; const data = await list(); // or chain off that list to get additional details const items = await list.items.top(2)(); defaultDocumentLibrary \u00b6 Get a reference the default documents library of a web import { IList } from \"@pnp/sp/lists\"; const list: IList = web.defaultDocumentLibrary; customListTemplates \u00b6 Gets the collection of all list definitions and list templates that are available import { IList } from \"@pnp/sp/lists\"; const templates = await web.customListTemplates(); // odata operators chain off the collection as expected const templates2 = await web.customListTemplates.select(\"Title\")(); getList \u00b6 Gets a list by server relative url (list's root folder) import { IList } from \"@pnp/sp/lists\"; const list: IList = web.getList(\"/sites/dev/lists/test\"); const listData = list(); getCatalog \u00b6 Returns the list gallery on the site Name Value WebTemplateCatalog 111 WebPartCatalog 113 ListTemplateCatalog 114 MasterPageCatalog 116 SolutionCatalog 121 ThemeCatalog 123 DesignCatalog 124 AppDataCatalog 125 import { IList } from \"@pnp/sp/lists\"; const templateCatalog: IList = await web.getCatalog(111); const themeCatalog: IList = await web.getCatalog(123); navigation imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/navigation\"; Selective 2 import \"@pnp/sp/navigation/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; navigation \u00b6 Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar import { INavigation } from \"@pnp/sp/navigation\"; const nav: INavigation = web.navigation; const navData = await nav(); regional-settings imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/regional-settings\"; Selective 2 import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRegionalSettings } from \"@pnp/sp/navigation\"; const settings: IRegionalSettings = web.regionalSettings; const settingsData = await settings(); related-items imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/related-items\"; Selective 2 import \"@pnp/sp/related-items/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRelatedItemManager, IRelatedItem } from \"@pnp/sp/related-items\"; const manager: IRelatedItemManager = web.relatedItems; const data: IRelatedItem[] = await manager.getRelatedItems(\"{list name}\", 4); security imports \u00b6 Please see information around the available security methods in the security article . sharing imports \u00b6 Please see information around the available sharing methods in the sharing article . site-groups imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/site-groups\"; Selective 2 import \"@pnp/sp/site-groups/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; siteGroups \u00b6 The site groups const groups = await web.siteGroups(); const groups2 = await web.siteGroups.top(2)(); associatedOwnerGroup \u00b6 The web's owner group const group = await web.associatedOwnerGroup(); const users = await web.associatedOwnerGroup.users(); associatedMemberGroup \u00b6 The web's member group const group = await web.associatedMemberGroup(); const users = await web.associatedMemberGroup.users(); associatedVisitorGroup \u00b6 The web's visitor group const group = await web.associatedVisitorGroup(); const users = await web.associatedVisitorGroup.users(); createDefaultAssociatedGroups \u00b6 Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\"); // copy the role assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", true); // don't clear sub assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, false); // specify secondary owner, don't copy permissions, clear sub scopes await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, true, \"{second owner login}\"); site-users imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/site-users\"; Selective 2 import \"@pnp/sp/site-users/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; siteUsers \u00b6 The site users const users = await web.siteUsers(); const users2 = await web.siteUsers.top(5)(); const users3 = await web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent(\"i:0#.f|m\")}')`)(); currentUser \u00b6 Information on the current user const user = await web.currentUser(); // check the login name of the current user const user2 = await web.currentUser.select(\"LoginName\")(); ensureUser \u00b6 Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web import { IWebEnsureUserResult } from \"@pnp/sp/site-users/\"; const result: IWebEnsureUserResult = await web.ensureUser(\"i:0#.f|membership|user@domain.onmicrosoft.com\"); getUserById \u00b6 Returns the user corresponding to the specified member identifier for the current web import { ISiteUser } from \"@pnp/sp/site-users/\"; const user: ISiteUser = web.getUserById(23); const userData = await user(); const userData2 = await user.select(\"LoginName\")(); user-custom-actions imports \u00b6 Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; userCustomActions \u00b6 Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection import { IUserCustomActions } from \"@pnp/sp/user-custom-actions\"; const actions: IUserCustomActions = web.userCustomActions; const actionsData = await actions(); IWebInfosData \u00b6 Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations. interface IWebInfosData { Configuration: number; Created: string; Description: string; Id: string; Language: number; LastItemModifiedDate: string; LastItemUserModifiedDate: string; ServerRelativeUrl: string; Title: string; WebTemplate: string; WebTemplateId: number; }","title":"@pnp/sp/webs"},{"location":"v2/sp/webs/#pnpspwebs","text":"Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.","title":"@pnp/sp/webs"},{"location":"v2/sp/webs/#iwebs","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Webs, IWebs } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Webs, IWebs } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Webs, IWebs } from \"@pnp/sp/presets/core\";","title":"IWebs"},{"location":"v2/sp/webs/#add-web","text":"Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions. import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; const result = await sp.web.webs.add(\"title\", \"subweb1\"); // show the response from the server when adding the web console.log(result.data); // we can immediately operate on the new web result.web.select(\"Title\")().then((w: IWebAddResult) => { // show our title console.log(w.Title); }); import { sp } from \"@pnp/sp\"; import { IWebAddResult } from \"@pnp/sp/webs\"; // create a German language wiki site with title, url, description, which does not inherit permissions sp.web.webs.add(\"wiki\", \"subweb2\", \"a wiki web\", \"WIKI#0\", 1031, false).then((w: IWebAddResult) => { // ... });","title":"Add Web"},{"location":"v2/sp/webs/#iweb","text":"Scenario Import Statement Selective 1 import { sp } from \"@pnp/sp\"; import { Web, IWeb } from \"@pnp/sp/webs\"; Selective 2 import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; Preset: Core import { sp, Web, IWeb } from \"@pnp/sp/presets/core\";","title":"IWeb"},{"location":"v2/sp/webs/#access-a-web","text":"There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named \"web\" which represents an IWeb instance - regardless of how it was initially accessed. Access the web from the imported \"sp\" object using selective import: import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'all' preset import { sp } from \"@pnp/sp/presets/all\"; const r = await sp.web(); Access the web from the imported \"sp\" using the 'core' preset import { sp } from \"@pnp/sp/presets/core\"; const r = await sp.web(); Create a web instance using the factory function import { Web } from \"@pnp/sp/webs\"; const web = Web(\"https://something.sharepoint.com/sites/dev\"); const r = await web();","title":"Access a Web"},{"location":"v2/sp/webs/#webs","text":"Access the child webs collection of this web const webs = web.webs();","title":"webs"},{"location":"v2/sp/webs/#get-a-webs-properties","text":"// basic get of the webs properties const props = await web(); // use odata operators to get specific fields const props2 = await web.select(\"Title\")(); // type the result to match what you are requesting const props3 = await web.select(\"Title\")<{ Title: string }>();","title":"Get A Web's properties"},{"location":"v2/sp/webs/#getparentweb","text":"Get the data and IWeb instance for the parent web for the given web instance import { IOpenWebByIdResult } from \"@pnp/sp/sites\"; const web: IOpenWebByIdResult = web.getParentWeb();","title":"getParentWeb"},{"location":"v2/sp/webs/#getsubwebsfilteredforcurrentuser","text":"Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member. const subWebs = await web.getSubwebsFilteredForCurrentUser()(); // apply odata operations to the collection const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select(\"Title\", \"Language\").orderBy(\"Created\", true)(); Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.","title":"getSubwebsFilteredForCurrentUser"},{"location":"v2/sp/webs/#allproperties","text":"Allows access to the web's all properties collection. This is readonly in REST. const props = await web.allProperties(); // select certain props const props2 = await web.allProperties.select(\"prop1\", \"prop2\")();","title":"allProperties"},{"location":"v2/sp/webs/#webinfos","text":"Gets a collection of WebInfos for this web's subwebs const infos = await web.webinfos(); // or select certain fields const infos2 = await web.webinfos.select(\"Title\", \"Description\")(); // or filter const infos3 = await web.webinfos.filter(\"Title eq 'MyWebTitle'\")(); // or both const infos4 = await web.webinfos.select(\"Title\", \"Description\").filter(\"Title eq 'MyWebTitle'\")(); // get the top 4 ordered by Title const infos5 = await web.webinfos.top(4).orderBy(\"Title\")(); Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.","title":"webinfos"},{"location":"v2/sp/webs/#update","text":"Updates this web instance with the supplied properties // update the web's title and description const result = await web.update({ Title: \"New Title\", Description: \"My new description\", }); // a project implementation could wrap the update to provide type information for your expected fields: import { IWebUpdateResult } from \"@pnp/sp/webs\"; interface IWebUpdateProps { Title: string; Description: string; } function updateWeb(props: IWebUpdateProps): Promise { web.update(props); }","title":"update"},{"location":"v2/sp/webs/#delete-a-web","text":"await web.delete();","title":"Delete a Web"},{"location":"v2/sp/webs/#applytheme","text":"Applies the theme specified by the contents of each of the files specified in the arguments to the site import { combine } from \"@pnp/core\"; // we are going to apply the theme to this sub web as an example const web = Web(\"https://{tenant}.sharepoint.com/sites/dev/subweb\"); // the urls to the color and font need to both be from the catalog at the root // these urls can be constants or calculated from existing urls const colorUrl = combine(\"/\", \"sites/dev\", \"_catalogs/theme/15/palette011.spcolor\"); // this gives us the same result const fontUrl = \"/sites/dev/_catalogs/theme/15/fontscheme007.spfont\"; // apply the font and color, no background image, and don't share this theme await web.applyTheme(colorUrl, fontUrl, \"\", false);","title":"applyTheme"},{"location":"v2/sp/webs/#applywebtemplate-availablewebtemplates","text":"Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios. const templates = (await web.availableWebTemplates().select(\"Name\")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name)); // apply the wiki template const template = templates.length > 0 ? templates[0].Name : \"STS#0\"; await web.applyWebTemplate(template);","title":"applyWebTemplate & availableWebTemplates"},{"location":"v2/sp/webs/#getchanges","text":"Returns the collection of changes from the change log that have occurred within the web, based on the specified query. // get the web changes including add, update, and delete const changes = await web.getChanges({ Add: true, ChangeTokenEnd: null, ChangeTokenStart: null, DeleteObject: true, Update: true, Web: true, });","title":"getChanges"},{"location":"v2/sp/webs/#maptoicon","text":"Returns the name of the image file for the icon that is used to represent the specified file import { combine } from \"@pnp/core\"; const iconFileName = await web.mapToIcon(\"test.docx\"); // iconPath === \"icdocx.png\" // which you can need to map to a real url const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`; // OR dynamically const webData = await sp.web.select(\"Url\")(); const iconFullPath2 = combine(webData.Url, \"_layouts\", \"images\", iconFileName); // OR within SPFx using the context const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, \"_layouts\", \"images\", iconFileName); // You can also set size // 16x16 pixels = 0, 32x32 pixels = 1 const icon32FileName = await web.mapToIcon(\"test.docx\", 1);","title":"mapToIcon"},{"location":"v2/sp/webs/#storage-entities","text":"import { sp } from \"@pnp/sp\"; import \"@pnp/sp/appcatalog\"; import { IStorageEntity } from \"@pnp/sp/webs\"; // needs to be unique, GUIDs are great const key = \"my-storage-key\"; // read an existing entity const entity: IStorageEntity = await web.getStorageEntity(key); // setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site // you can get the tenant app catalog using the getTenantAppCatalogWeb const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb(); tenantAppCatalogWeb.setStorageEntity(key, \"new value\"); // set other properties tenantAppCatalogWeb.setStorageEntity(key, \"another value\", \"description\", \"comments\"); const entity2: IStorageEntity = await web.getStorageEntity(key); /* entity2 === { Value: \"another value\", Comment: \"comments\"; Description: \"description\", }; */ // you can also remove a storage entity await tenantAppCatalogWeb.removeStorageEntity(key);","title":"storage entities"},{"location":"v2/sp/webs/#appcatalog-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/appcatalog\"; Selective 2 import \"@pnp/sp/appcatalog/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"appcatalog imports"},{"location":"v2/sp/webs/#getappcatalog","text":"Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url. import { IApp } from \"@pnp/sp/appcatalog\"; const appWeb = web.getAppCatalog(); // appWeb url === web url const app: IApp = appWeb.getAppById(\"{your app id}\"); const appWeb2 = web.getAppCatalog(\"https://tenant.sharepoing.com/sites/someappcatalog\"); // appWeb2 url === \"https://tenant.sharepoing.com/sites/someappcatalog\"","title":"getAppCatalog"},{"location":"v2/sp/webs/#client-side-pages-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/client-side-pages\"; Selective 2 import \"@pnp/sp/client-side-pages/web\"; Preset: All import { sp, Web, IWeb } from \"@pnp/sp/presets/all\"; You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article. import { sp } from \"@pnp/sp\"; import \"@pnp/sp/webs\"; import \"@pnp/sp/clientside-pages/web\"; // simplest add a page example const page = await sp.web.addClientsidePage(\"mypage1\"); // simplest load a page example const page = await sp.web.loadClientsidePage(\"/sites/dev/sitepages/mypage3.aspx\");","title":"client-side-pages imports"},{"location":"v2/sp/webs/#content-type-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/content-types\"; Selective 2 import \"@pnp/sp/content-types/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"content-type imports"},{"location":"v2/sp/webs/#contenttypes","text":"Allows access to the collection of content types in this web. const cts = await web.contentTypes(); // you can also select fields and use other odata operators const cts2 = await web.contentTypes.select(\"Name\")();","title":"contentTypes"},{"location":"v2/sp/webs/#features-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/features\"; Selective 2 import \"@pnp/sp/features/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"features imports"},{"location":"v2/sp/webs/#features","text":"Allows access to the collection of content types in this web. const features = await web.features();","title":"features"},{"location":"v2/sp/webs/#fields-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/fields\"; Selective 2 import \"@pnp/sp/fields/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"fields imports"},{"location":"v2/sp/webs/#fields","text":"Allows access to the collection of fields in this web. const fields = await web.fields();","title":"fields"},{"location":"v2/sp/webs/#files-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/files\"; Selective 2 import \"@pnp/sp/files/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"files imports"},{"location":"v2/sp/webs/#getfilebyserverrelativeurl","text":"Gets a file by server relative url import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativeUrl(\"/sites/dev/library/myfile.docx\");","title":"getFileByServerRelativeUrl"},{"location":"v2/sp/webs/#getfilebyserverrelativepath","text":"Gets a file by server relative url if your file name contains # and % characters import { IFile } from \"@pnp/sp/files\"; const file: IFile = web.getFileByServerRelativePath(\"/sites/dev/library/my # file%.docx\");","title":"getFileByServerRelativePath"},{"location":"v2/sp/webs/#folders-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/folders\"; Selective 2 import \"@pnp/sp/folders/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"folders imports"},{"location":"v2/sp/webs/#folders","text":"Gets the collection of folders in this web const folders = await web.folders(); // you can also filter and select as with any collection const folders2 = await web.folders.select(\"ServerRelativeUrl\", \"TimeLastModified\").filter(\"ItemCount gt 0\")(); // or get the most recently modified folder const folders2 = await web.folders.orderBy(\"TimeLastModified\").top(1)();","title":"folders"},{"location":"v2/sp/webs/#rootfolder","text":"Gets the root folder of the web const folder = await web.rootFolder();","title":"rootFolder"},{"location":"v2/sp/webs/#getfolderbyserverrelativeurl","text":"Gets a folder by server relative url import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativeUrl(\"/sites/dev/library\");","title":"getFolderByServerRelativeUrl"},{"location":"v2/sp/webs/#getfolderbyserverrelativepath","text":"Gets a folder by server relative url if your folder name contains # and % characters import { IFolder } from \"@pnp/sp/folders\"; const folder: IFolder = web.getFolderByServerRelativePath(\"/sites/dev/library/my # folder%/\");","title":"getFolderByServerRelativePath"},{"location":"v2/sp/webs/#hubsites-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/hubsites\"; Selective 2 import \"@pnp/sp/hubsites/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"hubsites imports"},{"location":"v2/sp/webs/#hubsitedata","text":"Gets hub site data for the current web import { IHubSiteWebData } from \"@pnp/sp/hubsites\"; // get the data and force a refresh const data: IHubSiteWebData = await web.hubSiteData(true);","title":"hubSiteData"},{"location":"v2/sp/webs/#synchubsitetheme","text":"Applies theme updates from the parent hub site collection await web.syncHubSiteTheme();","title":"syncHubSiteTheme"},{"location":"v2/sp/webs/#lists-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/lists\"; Selective 2 import \"@pnp/sp/lists/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; Preset: Core import { sp } from \"@pnp/sp/presets/core\";","title":"lists imports"},{"location":"v2/sp/webs/#lists","text":"Gets the collection of all lists that are contained in the Web site import { ILists } from \"@pnp/sp/lists\"; const lists: ILists = web.lists; // you can always order the lists and select properties const data = await lists.select(\"Title\").orderBy(\"Title\")(); // and use other odata operators as well const data2 = await web.lists.top(3).orderBy(\"LastItemModifiedDate\")();","title":"lists"},{"location":"v2/sp/webs/#siteuserinfolist","text":"Gets the UserInfo list of the site collection that contains the Web site import { IList } from \"@pnp/sp/lists\"; const list: IList = web.siteUserInfoList; const data = await list(); // or chain off that list to get additional details const items = await list.items.top(2)();","title":"siteUserInfoList"},{"location":"v2/sp/webs/#defaultdocumentlibrary","text":"Get a reference the default documents library of a web import { IList } from \"@pnp/sp/lists\"; const list: IList = web.defaultDocumentLibrary;","title":"defaultDocumentLibrary"},{"location":"v2/sp/webs/#customlisttemplates","text":"Gets the collection of all list definitions and list templates that are available import { IList } from \"@pnp/sp/lists\"; const templates = await web.customListTemplates(); // odata operators chain off the collection as expected const templates2 = await web.customListTemplates.select(\"Title\")();","title":"customListTemplates"},{"location":"v2/sp/webs/#getlist","text":"Gets a list by server relative url (list's root folder) import { IList } from \"@pnp/sp/lists\"; const list: IList = web.getList(\"/sites/dev/lists/test\"); const listData = list();","title":"getList"},{"location":"v2/sp/webs/#getcatalog","text":"Returns the list gallery on the site Name Value WebTemplateCatalog 111 WebPartCatalog 113 ListTemplateCatalog 114 MasterPageCatalog 116 SolutionCatalog 121 ThemeCatalog 123 DesignCatalog 124 AppDataCatalog 125 import { IList } from \"@pnp/sp/lists\"; const templateCatalog: IList = await web.getCatalog(111); const themeCatalog: IList = await web.getCatalog(123);","title":"getCatalog"},{"location":"v2/sp/webs/#navigation-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/navigation\"; Selective 2 import \"@pnp/sp/navigation/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"navigation imports"},{"location":"v2/sp/webs/#navigation","text":"Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar import { INavigation } from \"@pnp/sp/navigation\"; const nav: INavigation = web.navigation; const navData = await nav();","title":"navigation"},{"location":"v2/sp/webs/#regional-settings-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/regional-settings\"; Selective 2 import \"@pnp/sp/regional-settings/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRegionalSettings } from \"@pnp/sp/navigation\"; const settings: IRegionalSettings = web.regionalSettings; const settingsData = await settings();","title":"regional-settings imports"},{"location":"v2/sp/webs/#related-items-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/related-items\"; Selective 2 import \"@pnp/sp/related-items/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\"; import { IRelatedItemManager, IRelatedItem } from \"@pnp/sp/related-items\"; const manager: IRelatedItemManager = web.relatedItems; const data: IRelatedItem[] = await manager.getRelatedItems(\"{list name}\", 4);","title":"related-items imports"},{"location":"v2/sp/webs/#security-imports","text":"Please see information around the available security methods in the security article .","title":"security imports"},{"location":"v2/sp/webs/#sharing-imports","text":"Please see information around the available sharing methods in the sharing article .","title":"sharing imports"},{"location":"v2/sp/webs/#site-groups-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/site-groups\"; Selective 2 import \"@pnp/sp/site-groups/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"site-groups imports"},{"location":"v2/sp/webs/#sitegroups","text":"The site groups const groups = await web.siteGroups(); const groups2 = await web.siteGroups.top(2)();","title":"siteGroups"},{"location":"v2/sp/webs/#associatedownergroup","text":"The web's owner group const group = await web.associatedOwnerGroup(); const users = await web.associatedOwnerGroup.users();","title":"associatedOwnerGroup"},{"location":"v2/sp/webs/#associatedmembergroup","text":"The web's member group const group = await web.associatedMemberGroup(); const users = await web.associatedMemberGroup.users();","title":"associatedMemberGroup"},{"location":"v2/sp/webs/#associatedvisitorgroup","text":"The web's visitor group const group = await web.associatedVisitorGroup(); const users = await web.associatedVisitorGroup.users();","title":"associatedVisitorGroup"},{"location":"v2/sp/webs/#createdefaultassociatedgroups","text":"Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\"); // copy the role assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", true); // don't clear sub assignments await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, false); // specify secondary owner, don't copy permissions, clear sub scopes await web.createDefaultAssociatedGroups(\"Contoso\", \"{first owner login}\", false, true, \"{second owner login}\");","title":"createDefaultAssociatedGroups"},{"location":"v2/sp/webs/#site-users-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/site-users\"; Selective 2 import \"@pnp/sp/site-users/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"site-users imports"},{"location":"v2/sp/webs/#siteusers","text":"The site users const users = await web.siteUsers(); const users2 = await web.siteUsers.top(5)(); const users3 = await web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent(\"i:0#.f|m\")}')`)();","title":"siteUsers"},{"location":"v2/sp/webs/#currentuser","text":"Information on the current user const user = await web.currentUser(); // check the login name of the current user const user2 = await web.currentUser.select(\"LoginName\")();","title":"currentUser"},{"location":"v2/sp/webs/#ensureuser","text":"Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web import { IWebEnsureUserResult } from \"@pnp/sp/site-users/\"; const result: IWebEnsureUserResult = await web.ensureUser(\"i:0#.f|membership|user@domain.onmicrosoft.com\");","title":"ensureUser"},{"location":"v2/sp/webs/#getuserbyid","text":"Returns the user corresponding to the specified member identifier for the current web import { ISiteUser } from \"@pnp/sp/site-users/\"; const user: ISiteUser = web.getUserById(23); const userData = await user(); const userData2 = await user.select(\"LoginName\")();","title":"getUserById"},{"location":"v2/sp/webs/#user-custom-actions-imports","text":"Scenario Import Statement Selective 1 import \"@pnp/sp/user-custom-actions\"; Selective 2 import \"@pnp/sp/user-custom-actions/web\"; Preset: All import { sp } from \"@pnp/sp/presets/all\";","title":"user-custom-actions imports"},{"location":"v2/sp/webs/#usercustomactions","text":"Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection import { IUserCustomActions } from \"@pnp/sp/user-custom-actions\"; const actions: IUserCustomActions = web.userCustomActions; const actionsData = await actions();","title":"userCustomActions"},{"location":"v2/sp/webs/#iwebinfosdata","text":"Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations. interface IWebInfosData { Configuration: number; Created: string; Description: string; Id: string; Language: number; LastItemModifiedDate: string; LastItemUserModifiedDate: string; ServerRelativeUrl: string; Title: string; WebTemplate: string; WebTemplateId: number; }","title":"IWebInfosData"},{"location":"v2/sp-addinhelpers/","text":"@pnp/sp-addinhelpers \u00b6 This module contains classes to allow use of the libraries within a SharePoint add-in. Getting Started \u00b6 Install the library and all dependencies, npm install @pnp/sp @pnp/sp-addinhelpers --save Now you can make requests to the host web from your add-in using the crossDomainWeb method. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4)); Library Topics \u00b6 SPRequestExecutorClient SPRestAddIn","title":"@pnp/sp-addinhelpers"},{"location":"v2/sp-addinhelpers/#pnpsp-addinhelpers","text":"This module contains classes to allow use of the libraries within a SharePoint add-in.","title":"@pnp/sp-addinhelpers"},{"location":"v2/sp-addinhelpers/#getting-started","text":"Install the library and all dependencies, npm install @pnp/sp @pnp/sp-addinhelpers --save Now you can make requests to the host web from your add-in using the crossDomainWeb method. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4));","title":"Getting Started"},{"location":"v2/sp-addinhelpers/#library-topics","text":"SPRequestExecutorClient SPRestAddIn","title":"Library Topics"},{"location":"v2/sp-addinhelpers/sp-request-executor-client/","text":"@pnp/sp-addinhelpers/sprequestexecutorclient \u00b6 The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request. Setup \u00b6 To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor sp.crossDomainWeb(addInWenUrl, hostWebUrl)().then(w => { console.log(JSON.stringify(w, null, 4)); });","title":"@pnp/sp-addinhelpers/sprequestexecutorclient"},{"location":"v2/sp-addinhelpers/sp-request-executor-client/#pnpsp-addinhelperssprequestexecutorclient","text":"The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request.","title":"@pnp/sp-addinhelpers/sprequestexecutorclient"},{"location":"v2/sp-addinhelpers/sp-request-executor-client/#setup","text":"To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web. // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor sp.crossDomainWeb(addInWenUrl, hostWebUrl)().then(w => { console.log(JSON.stringify(w, null, 4)); });","title":"Setup"},{"location":"v2/sp-addinhelpers/sp-rest-addin/","text":"@pnp/sp-addinhelpers/sprestaddin \u00b6 This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4));","title":"@pnp/sp-addinhelpers/sprestaddin"},{"location":"v2/sp-addinhelpers/sp-rest-addin/#pnpsp-addinhelperssprestaddin","text":"This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods import { sp, SPRequestExecutorClient } from \"@pnp/sp-addinhelpers\"; // this only needs to be done once within your application sp.setup({ sp: { fetchClientFactory: () => { return new SPRequestExecutorClient(); } } }); // now we need to use the crossDomainWeb method to make our requests to the host web const addInWenUrl = \"{The add-in web url, likely from the query string}\"; const hostWebUrl = \"{The host web url, likely from the query string}\"; // make requests into the host web via the SP.RequestExecutor const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)(); console.log(JSON.stringify(w, null, 4));","title":"@pnp/sp-addinhelpers/sprestaddin"}]} \ No newline at end of file diff --git a/v2/sitemap.xml b/v2/sitemap.xml new file mode 100644 index 000000000..d5c63d87f --- /dev/null +++ b/v2/sitemap.xml @@ -0,0 +1,483 @@ + + + https://pnp.github.io/pnpjs/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/getting-started/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/news/2020-year-in-review/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/getting-started/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/transition-guide/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/npm-scripts/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/SPFx-on-premises/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs-support/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/configuration/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/selective-imports/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/custom-bundle/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/ie11-mode/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/invokable/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/polyfill/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/settings/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/concepts/error-handling/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/client-spfx/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/msaljsclient/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/adaljsclient/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/client-spa/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/server-nodejs/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/sp-app-registration/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/bearertokenclient/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/authentication/lambdaclient/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/packages/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/collections/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/custom-httpclientimpl/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/libconfig/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/netutil/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/storage/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/common/util/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/config-store/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/config-store/configuration/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/config-store/providers/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/groups/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/insights/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/contacts/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/calendars/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/directoryobjects/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/invitations/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/onedrive/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/outlook/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/photos/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/planner/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/search/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/subscriptions/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/teams/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/graph/users/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/logging/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/sp-fetch-client/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/adal-fetch-client/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/bearer-token-fetch-client/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/provider-hosted-app/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/sp-extensions/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/nodejs/proxy/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/caching/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/core/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/odata-batch/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/extensions/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/debug/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/parsers/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/pipeline/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/odata/queryable/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/pnpjs/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/alias-parameters/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/alm/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/attachments/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/clientside-pages/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/column-defaults/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/comments-likes/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/content-types/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/entity-merging/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/features/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/fields/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/files/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/folders/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/forms/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/hubsites/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/items/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/lists/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/navigation/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/permissions/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/profiles/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/regional-settings/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/related-items/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/search/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/security/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/sharing/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/site-designs/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/site-groups/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/site-scripts/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/site-users/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/sites/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/social/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/sp-utilities-utility/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/subscriptions/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/taxonomy/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/tenant-properties/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/user-custom-actions/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/views/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/webs/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp/custom-irequestclient/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp-addinhelpers/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp-addinhelpers/sp-request-executor-client/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/sp-addinhelpers/sp-rest-addin/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/setup-dev-machine/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/local-debug-configuration/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/debugging/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/extending-the-library/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/debug-tests/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/documentation/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/contributing/pull-requests/ + 2021-10-01 + daily + + https://pnp.github.io/pnpjs/v1/ + 2021-10-01 + daily + + \ No newline at end of file diff --git a/v2/sitemap.xml.gz b/v2/sitemap.xml.gz new file mode 100644 index 000000000..2fefcbc45 Binary files /dev/null and b/v2/sitemap.xml.gz differ diff --git a/v2/sp-addinhelpers/index.html b/v2/sp-addinhelpers/index.html new file mode 100644 index 000000000..c71159f87 --- /dev/null +++ b/v2/sp-addinhelpers/index.html @@ -0,0 +1,2274 @@ + + + + + + + + + + + + + + + + + + sp-addinhelpers - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp-addinhelpers

    +

    npm version

    +

    This module contains classes to allow use of the libraries within a SharePoint add-in.

    +

    Getting Started

    +

    Install the library and all dependencies,

    +

    npm install @pnp/sp @pnp/sp-addinhelpers --save

    +

    Now you can make requests to the host web from your add-in using the crossDomainWeb method.

    +
    // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods
    +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers";
    +
    +// this only needs to be done once within your application
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPRequestExecutorClient();
    +        }
    +    }
    +});
    +
    +// now we need to use the crossDomainWeb method to make our requests to the host web
    +const addInWenUrl = "{The add-in web url, likely from the query string}";
    +const hostWebUrl = "{The host web url, likely from the query string}";
    +
    +// make requests into the host web via the SP.RequestExecutor
    +const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)();
    +console.log(JSON.stringify(w, null, 4));
    +
    +

    Library Topics

    + + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp-addinhelpers/sp-request-executor-client/index.html b/v2/sp-addinhelpers/sp-request-executor-client/index.html new file mode 100644 index 000000000..d3ae46a54 --- /dev/null +++ b/v2/sp-addinhelpers/sp-request-executor-client/index.html @@ -0,0 +1,2253 @@ + + + + + + + + + + + + + + + + + + SPRequestExecutorClient - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp-addinhelpers/sprequestexecutorclient

    +

    The SPRequestExecutorClient is an implementation of the HttpClientImpl interface that facilitates requests to SharePoint from an add-in. It relies on the SharePoint SP product libraries being present to allow use of the SP.RequestExecutor to make the request.

    +

    Setup

    +

    To use the client you need to set it using the fetch client factory using the setup method as shown below. This is only required when working within a SharePoint add-in web.

    +
    // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods
    +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers";
    +
    +// this only needs to be done once within your application
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPRequestExecutorClient();
    +        }
    +    }
    +});
    +
    +// now we need to use the crossDomainWeb method to make our requests to the host web
    +const addInWenUrl = "{The add-in web url, likely from the query string}";
    +const hostWebUrl = "{The host web url, likely from the query string}";
    +
    +// make requests into the host web via the SP.RequestExecutor
    +sp.crossDomainWeb(addInWenUrl, hostWebUrl)().then(w => {
    +    console.log(JSON.stringify(w, null, 4));
    +});
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp-addinhelpers/sp-rest-addin/index.html b/v2/sp-addinhelpers/sp-rest-addin/index.html new file mode 100644 index 000000000..5404434d6 --- /dev/null +++ b/v2/sp-addinhelpers/sp-rest-addin/index.html @@ -0,0 +1,2206 @@ + + + + + + + + + + + + + + + + + + SPRestAddIn - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp-addinhelpers/sprestaddin

    +

    This class extends the sp export from @pnp/sp and adds in the methods required to make cross domain calls

    +
    // note we are getting the sp variable from this library, it extends the sp export from @pnp/sp to add the required helper methods
    +import { sp, SPRequestExecutorClient } from "@pnp/sp-addinhelpers";
    +
    +// this only needs to be done once within your application
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPRequestExecutorClient();
    +        }
    +    }
    +});
    +
    +// now we need to use the crossDomainWeb method to make our requests to the host web
    +const addInWenUrl = "{The add-in web url, likely from the query string}";
    +const hostWebUrl = "{The host web url, likely from the query string}";
    +
    +// make requests into the host web via the SP.RequestExecutor
    +const w = await sp.crossDomainWeb(addInWenUrl, hostWebUrl)();
    +console.log(JSON.stringify(w, null, 4));
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/alias-parameters/index.html b/v2/sp/alias-parameters/index.html new file mode 100644 index 000000000..2c472d7c1 --- /dev/null +++ b/v2/sp/alias-parameters/index.html @@ -0,0 +1,2330 @@ + + + + + + + + + + + + + + + + + + Alias Parameters - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp - Aliased Parameters

    +

    Within the @pnp/sp api you can alias any of the parameters so they will be written into the querystring. This is most helpful if you are hitting up against the url length limits when working with files and folders.

    +

    To alias a parameter you include the label name, a separator ("::") and the value in the string. You also need to prepend a "!" to the string to trigger the replacement. You can see this below, as well as the string that will be generated. Labels must start with a "@" followed by a letter. It is also your responsibility to ensure that the aliases you supply do not conflict, for example if you use "@p1" you should use "@p2" for a second parameter alias in the same query.

    +

    Construct a parameter alias

    +

    Pattern: !@{label name}::{value}

    +

    Example: "!@p1::\sites\dev" or "!@p2::\text.txt"

    +

    Example without aliasing

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +// still works as expected, no aliasing
    +const query = sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/").files.select("Title").top(3);
    +
    +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files
    +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl('/sites/dev/Shared Documents/')/files?$select=Title&$top=3
    +
    +const r = await query();
    +console.log(r);;
    +
    +

    Example with aliasing

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +// same query with aliasing
    +const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
    +
    +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
    +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
    +
    +const r = await query();
    +console.log(r);
    +
    +

    Example with aliasing and batching

    +

    Aliasing is supported with batching as well:

    +
    import { sp } from "@pnp/sp/presets/all";
    +// same query with aliasing and batching
    +const batch = sp.web.createBatch();
    +
    +const query = sp.web.getFolderByServerRelativeUrl("!@p1::/sites/dev/Shared Documents/").files.select("Title").top(3);
    +
    +console.log(query.toUrl()); // _api/web/getFolderByServerRelativeUrl('!@p1::/sites/dev/Shared Documents/')/files
    +console.log(query.toUrlAndQuery()); // _api/web/getFolderByServerRelativeUrl(@p1)/files?@p1='/sites/dev/Shared Documents/'&$select=Title&$top=3
    +
    +query.inBatch(batch)().then(r => {
    +
    +    console.log(r);
    +});
    +
    +batch.execute();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/alm/index.html b/v2/sp/alm/index.html new file mode 100644 index 000000000..4295e9138 --- /dev/null +++ b/v2/sp/alm/index.html @@ -0,0 +1,2433 @@ + + + + + + + + + + + + + + + + + + ALM api - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/appcatalog

    +

    The ALM api allows you to manage app installations both in the tenant app catalog and individual site app catalogs. Some of the methods are still in beta and as such may change in the future. This article outlines how to call this api using @pnp/sp. Remember all these actions are bound by permissions so it is likely most users will not have the rights to perform these ALM actions.

    +

    Understanding the App Catalog Hierarchy

    +

    Before you begin provisioning applications it is important to understand the relationship between a local web catalog and the tenant app catalog. Some of the methods described below only work within the context of the tenant app catalog web, such as adding an app to the catalog and the app actions retract, remove, and deploy. You can install, uninstall, and upgrade an app in any web. Read more in the official documentation.

    +

    Referencing an App Catalog

    +

    There are several ways using @pnp/sp to get a reference to an app catalog. These methods are to provide you the greatest amount of flexibility in gaining access to the app catalog. Ultimately each method produces an AppCatalog instance differentiated only by the web to which it points.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/appcatalog";
    +import "@pnp/sp/webs";
    +
    +// get the current context web's app catalog
    +const catalog = await sp.web.getAppCatalog()();
    +
    +// you can also chain off the app catalog
    +const apps = await sp.web.getAppCatalog()();
    +console.log(apps);
    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/appcatalog";
    +import "@pnp/sp/webs";
    +
    +// you can get the tenant app catalog (or any app catalog) by using the getTenantAppCatalogWeb method
    +const appCatWeb = await sp.getTenantAppCatalogWeb()();
    +const appCatalog = await appCatWeb.getAppCatalog()();
    +
    +// you can get the tenant app catalog (or any app catalog) by passing in a url
    +// get the tenant app catalog
    +const tenantCatalog = await sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/appcatalog")();
    +
    +// get a different app catalog
    +const catalog = await sp.web.getAppCatalog("https://mytenant.sharepoint.com/sites/anothersite")();
    +
    +
    // alternatively you can create a new app catalog instance directly by importing the AppCatalog class
    +import { IAppCatalog, AppCatalog } from '@pnp/sp/appcatalog';
    +
    +const catalog: IAppCatalog = await AppCatalog("https://mytenant.sharepoint.com/sites/apps")();
    +
    +
    // and finally you can combine use of the Web and AppCatalog classes to create an AppCatalog instance from an existing Web
    +import { Web } from '@pnp/sp/webs';
    +import { AppCatalog } from '@pnp/sp/appcatalog';
    +
    +const web = Web("https://mytenant.sharepoint.com/sites/apps");
    +const catalog = await AppCatalog(web)();
    +
    +

    The following examples make use of a variable "catalog" which is assumed to represent an AppCatalog instance obtained using one of the above methods, supporting code is omitted for brevity.

    +

    List Available Apps

    +

    The AppCatalog is itself a queryable collection so you can query this object directly to get a list of available apps. Also, the odata operators work on the catalog to sort, filter, and select.

    +
    // get available apps
    +await catalog();
    +
    +// get available apps selecting two fields
    +await catalog.select("Title", "Deployed")();
    +
    +

    Add an App

    +

    This action must be performed in the context of the tenant app catalog

    +
    // this represents the file bytes of the app package file
    +const blob = new Blob();
    +
    +// there is an optional third argument to control overwriting existing files
    +const r = await catalog.add("myapp.app", blob);
    +
    +// this is at its core a file add operation so you have access to the response data as well
    +// as a File instance representing the created file
    +console.log(JSON.stringify(r.data, null, 4));
    +
    +// all file operations are available
    +const nameData = await r.file.select("Name")();
    +
    +

    Get an App

    +

    You can get the details of a single app by GUID id. This is also the branch point to perform specific app actions

    +
    const app = await catalog.getAppById("5137dff1-0b79-4ebc-8af4-ca01f7bd393c")();
    +
    +

    Perform app actions

    +

    Remember: retract, deploy, and remove only work in the context of the tenant app catalog web. All of these methods return void and you can monitor success by wrapping the call in a try/catch block.

    +
    const myAppId = "5137dff1-0b79-4ebc-8af4-ca01f7bd393c";
    +
    +// deploy
    +await catalog.getAppById(myAppId).deploy();
    +
    +// retract
    +await catalog.getAppById(myAppId).retract();
    +
    +// install
    +await catalog.getAppById(myAppId).install();
    +
    +// uninstall
    +await catalog.getAppById(myAppId).uninstall();
    +
    +// upgrade
    +await catalog.getAppById(myAppId).upgrade();
    +
    +// remove
    +await catalog.getAppById(myAppId).remove();
    +
    +
    +

    Synchronize a solution/app to the Microsoft Teams App Catalog

    +

    By default this REST call requires the SharePoint item id of the app, not the app id. PnPjs will try to fetch the SharePoint item id by default. You can still use this the second parameter useSharePointItemId to pass your own item id in the first parameter id.

    +
    // Using the app id
    +await catalog.syncSolutionToTeams("5137dff1-0b79-4ebc-8af4-ca01f7bd393c");
    +
    +// Using the SharePoint apps item id
    +await catalog.syncSolutionToTeams("123", true);
    +
    +

    Notes

    +
      +
    • The app catalog is just a document library under the hood, so you can also perform non-ALM actions on the library if needed. But you should be aware of possible side-effects to the ALM life-cycle when doing so.
    • +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/attachments/index.html b/v2/sp/attachments/index.html new file mode 100644 index 000000000..679adc539 --- /dev/null +++ b/v2/sp/attachments/index.html @@ -0,0 +1,2504 @@ + + + + + + + + + + + + + + + + + + Attachments - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/attachments

    +

    The ability to attach file to list items allows users to track documents outside of a document library. You can use the PnP JS Core library to work with attachments as outlined below.

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/attachments";
    Preset: Allimport { sp, IFeatures, Features } from "@pnp/sp/presets/all";
    +

    Get attachments

    +
    import { sp } from "@pnp/sp";
    +import { IAttachmentInfo } from "@pnp/sp/attachments";
    +import { IItem } from "@pnp/sp/items/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
    +
    +// get all the attachments
    +const info: IAttachmentInfo[] = await item.attachmentFiles();
    +
    +// get a single file by file name
    +const info2: IAttachmentInfo = await item.attachmentFiles.getByName("file.txt")();
    +
    +// select specific properties using odata operators and use Pick to type the result
    +const info3: Pick<IAttachmentInfo, "ServerRelativeUrl">[] = await item.attachmentFiles.select("ServerRelativeUrl")();
    +
    +

    Add an Attachment

    +

    You can add an attachment to a list item using the add method. This method takes either a string, Blob, or ArrayBuffer.

    +
    import { sp } from "@pnp/sp";
    +import { IItem } from "@pnp/sp/items";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
    +
    +await item.attachmentFiles.add("file2.txt", "Here is my content");
    +
    +

    Add Multiple

    +

    This method allows you to pass an array of AttachmentFileInfo plain objects that will be added one at a time as attachments. Essentially automating the promise chaining.

    +
    import { sp } from "@pnp/sp";
    +import { IList } from "@pnp/sp/lists";
    +import { IAttachmentFileInfo } from "@pnp/sp/attachments";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const list: IList = sp.web.lists.getByTitle("MyList");
    +
    +let fileInfos: IAttachmentFileInfo[] = [];
    +
    +fileInfos.push({
    +    name: "My file name 1",
    +    content: "string, blob, or array"
    +});
    +
    +fileInfos.push({
    +    name: "My file name 2",
    +    content: "string, blob, or array"
    +});
    +
    +await list.items.getById(2).attachmentFiles.addMultiple(fileInfos);
    +
    +

    Delete Multiple

    +
    import { sp } from "@pnp/sp";
    +import { IList } from "./@pnp/sp/lists/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const list: IList = sp.web.lists.getByTitle("MyList");
    +
    +await list.items.getById(2).attachmentFiles.deleteMultiple("1.txt", "2.txt");
    +
    +

    Read Attachment Content

    +

    You can read the content of an attachment as a string, Blob, ArrayBuffer, or json using the methods supplied.

    +
    import { sp } from "@pnp/sp";
    +import { IItem } from "@pnp/sp/items/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
    +
    +const text = await item.attachmentFiles.getByName("file.txt").getText();
    +
    +// use this in the browser, does not work in nodejs
    +const blob = await item.attachmentFiles.getByName("file.mp4").getBlob();
    +
    +// use this in nodejs
    +const buffer = await item.attachmentFiles.getByName("file.mp4").getBuffer();
    +
    +// file must be valid json
    +const json = await item.attachmentFiles.getByName("file.json").getJSON();
    +
    +

    Update Attachment Content

    +

    You can also update the content of an attachment. This API is limited compared to the full file API - so if you need to upload large files consider using a document library.

    +
    import { sp } from "@pnp/sp";
    +import { IItem } from "@pnp/sp/items/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
    +
    +await item.attachmentFiles.getByName("file2.txt").setContent("My new content!!!");
    +
    +

    Delete Attachment

    +
    import { sp } from "@pnp/sp";
    +import { IItem } from "@pnp/sp/items/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
    +
    +await item.attachmentFiles.getByName("file2.txt").delete();
    +
    +

    Recycle Attachment

    +

    Delete the attachment and send it to recycle bin

    +
    import { sp } from "@pnp/sp";
    +import { IItem } from "@pnp/sp/items/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const item: IItem = sp.web.lists.getByTitle("MyList").items.getById(1);
    +
    +await item.attachmentFiles.getByName("file2.txt").recycle();
    +
    +

    Recycle Multiple Attachments

    +

    Delete multiple attachments and send them to recycle bin

    +
    import { sp } from "@pnp/sp";
    +import { IList } from "@pnp/sp/lists/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/attachments";
    +
    +const list: IList = sp.web.lists.getByTitle("MyList");
    +
    +await list.items.getById(2).attachmentFiles.recycleMultiple("1.txt","2.txt");
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/clientside-pages/index.html b/v2/sp/clientside-pages/index.html new file mode 100644 index 000000000..97ddb0fce --- /dev/null +++ b/v2/sp/clientside-pages/index.html @@ -0,0 +1,3477 @@ + + + + + + + + + + + + + + + + + + Client-side Pages - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/clientside-pages

    +

    The 'clientside-pages' module allows you to create, edit, and delete modern SharePoint pages. There are methods to update the page settings and add/remove client-side web parts.

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from "@pnp/sp/clientside-pages";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/clientside-pages";
    Preset: Allimport { sp, ClientsidePageFromFile, ClientsideText, ClientsideWebpartPropertyTypes, CreateClientsidePage, ClientsideWebpart, IClientsidePage } from "@pnp/sp/presets/all";
    +

    Create a new Page

    +

    You can create a new client-side page in several ways, all are equivalent.

    +

    Create using IWeb.addClientsidePage

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/clientside-pages/web";
    +import { PromotedState } from "@pnp/sp/clientside-pages";
    +
    +// Create a page providing a file name
    +const page = await sp.web.addClientsidePage("mypage1");
    +
    +// ... other operations on the page as outlined below
    +
    +// the page is initially not published, you must publish it so it appears for others users
    +await page.save();
    +
    +// include title and page layout
    +const page2 = await sp.web.addClientsidePage("mypage", "My Page Title", "Article");
    +
    +// you must publish the new page
    +await page2.save();
    +
    +// include title, page layout, and specifying the publishing status (Added in 2.0.4)
    +const page3 = await sp.web.addClientsidePage("mypage", "My Page Title", "Article", PromotedState.PromoteOnPublish);
    +
    +// you must publish the new page, after which the page will immediately be promoted to a news article
    +await page3.save();
    +
    +

    Create using CreateClientsidePage method

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { Web } from "@pnp/sp/webs";
    +import { CreateClientsidePage, PromotedState } from "@pnp/sp/clientside-pages";
    +
    +const page1 = await CreateClientsidePage(sp.web, "mypage2", "My Page Title");
    +
    +// you must publish the new page
    +await page1.save(true);
    +
    +// specify the page layout type parameter
    +const page2 = await CreateClientsidePage(sp.web, "mypage3", "My Page Title", "Article");
    +
    +// you must publish the new page
    +await page2.save();
    +
    +// specify the page layout type parameter while also specifying the publishing status (Added in 2.0.4)
    +const page2half = await CreateClientsidePage(sp.web, "mypage3", "My Page Title", "Article", PromotedState.PromoteOnPublish);
    +
    +// you must publish the new page, after which the page will immediately be promoted to a news article
    +await page2half.save();
    +
    +// use the web factory to create a page in a specific web
    +const page3 = await CreateClientsidePage(Web("https://{absolute web url}"), "mypage4", "My Page Title");
    +
    +// you must publish the new page
    +await page3.save();
    +
    +

    Load Pages

    +

    There are a few ways to load pages, each of which results in an IClientsidePage instance being returned.

    +

    Load using IWeb.loadClientsidePage

    +

    This method takes a server relative path to the page to load.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { Web } from "@pnp/sp/webs";
    +import "@pnp/sp/clientside-pages/web";
    +
    +// use from the sp.web fluent chain
    +const page = await sp.web.loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
    +
    +// use the web factory to target a specific web
    +const page2 = await Web("https://{absolute web url}").loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
    +
    +

    Load using ClientsidePageFromFile

    +

    This method takes an IFile instance and loads an IClientsidePage instance.

    +
    import { sp } from "@pnp/sp";
    +import { ClientsidePageFromFile } from "@pnp/sp/clientside-pages";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files/web";
    +
    +const page = await ClientsidePageFromFile(sp.web.getFileByServerRelativePath("/sites/dev/sitepages/mypage3.aspx"));
    +
    +

    Edit Sections and Columns

    +

    Client-side pages are made up of sections, columns, and controls. Sections contain columns which contain controls. There are methods to operate on these within the page, in addition to the standard array methods available in JavaScript. These samples use a variable page that is understood to be an IClientsidePage instance which is either created or loaded as outlined in previous sections.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// add two columns with factor 6 - this is a two column layout as the total factor in a section should add up to 12
    +const section1 = page.addSection();
    +section1.addColumn(6);
    +section1.addColumn(6);
    +
    +// create a three column layout in a new section
    +const section2 = page.addSection();
    +section2.addColumn(4);
    +section2.addColumn(4);
    +section2.addColumn(4);
    +
    +// publish our changes
    +await page.save();
    +
    +

    Manipulate Sections and Columns

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// drop all the columns in this section
    +// this will also DELETE all controls contained in the columns
    +page.sections[1].columns.length = 0;
    +
    +// create a new column layout
    +page.sections[1].addColumn(4);
    +page.sections[1].addColumn(8);
    +
    +// publish our changes
    +await page.save();
    +
    +

    Vertical Section

    +

    The vertical section, if on the page, is stored within the sections array. However, you access it slightly differently to make things easier.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// add or get a vertical section (handles case where section already exists)
    +const vertSection = page.addVerticalSection();
    +
    +// ****************************************************************
    +
    +// if you know or want to test if a vertical section is present:
    +if (page.hasVerticalSection) {
    +
    +    // access the vertical section (this method will NOT create the section if it does not exist)
    +    page.verticalSection.addControl(new ClientsideText("hello"));
    +} else {
    +
    +    const vertSection = page.addVerticalSection();
    +    vertSection.addControl(new ClientsideText("hello"));
    +}
    +
    +

    Reorder Sections

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// swap the order of two sections
    +// this will preserve the controls within the columns
    +page.sections = [page.sections[1], page.sections[0]];
    +
    +// publish our changes
    +await page.save();
    +
    +

    Reorder Columns

    +

    The sections and columns are arrays, so normal array operations work as expected

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// swap the order of two columns
    +// this will preserve the controls within the columns
    +page.sections[1].columns = [page.sections[1].columns[1], page.sections[1].columns[0]];
    +
    +// publish our changes
    +await page.save();
    +
    +

    Clientside Controls

    +

    Once you have your sections and columns defined you will want to add/edit controls within those columns.

    +

    Add Text Content

    +
    import { ClientsideText } from "@pnp/sp/clientside-pages";
    +
    +// our page instance
    +const page: IClientsidePage;
    +
    +page.addSection().addControl(new ClientsideText("@pnp/sp is a great library!"));
    +
    +await page.save();
    +
    +

    Add Controls

    +

    Adding controls involves loading the available client-side part definitions from the server or creating a text part.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/clientside-pages/web";
    +import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
    +
    +// this will be a ClientsidePageComponent array
    +// this can be cached on the client in production scenarios
    +const partDefs = await sp.web.getClientsideWebParts();
    +
    +// find the definition we want, here by id
    +const partDef = partDefs.filter(c => c.Id === "490d7c76-1824-45b2-9de3-676421c997fa");
    +
    +// optionally ensure you found the def
    +if (partDef.length < 1) {
    +    // we didn't find it so we throw an error
    +    throw new Error("Could not find the web part");
    +}
    +
    +// create a ClientWebPart instance from the definition
    +const part = ClientsideWebpart.fromComponentDef(partDef[0]);
    +
    +// set the properties on the web part. Here for the embed web part we only have to supply an embedCode - in this case a YouTube video.
    +// the structure of the properties varies for each web part and each version of a web part, so you will need to ensure you are setting
    +// the properties correctly
    +part.setProperties<{ embedCode: string }>({
    +    embedCode: "https://www.youtube.com/watch?v=IWQFZ7Lx-rg",
    +});
    +
    +// we add that part to a new section
    +page.addSection().addControl(part);
    +
    +await page.save();
    +
    +

    Handle Different Webpart's Settings

    +

    There are many ways that client side web parts are implemented and we can't provide handling within the library for all possibilities. This example shows how to handle a property set within the serverProcessedContent, in this case a List part's display title.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { ClientsideWebpart } from "@pnp/sp/clientside-pages";
    +
    +// we create a class to wrap our functionality in a reusable way
    +class ListWebpart extends ClientsideWebpart {
    +
    +  constructor(control: ClientsideWebpart) {
    +    super((<any>control).json);
    +  }
    +
    +  // add property getter/setter for what we need, in this case "listTitle" within searchablePlainTexts
    +  public get DisplayTitle(): string {
    +    return this.json.webPartData?.serverProcessedContent?.searchablePlainTexts?.listTitle || "";
    +  }
    +
    +  public set DisplayTitle(value: string) {
    +    this.json.webPartData.serverProcessedContent.searchablePlainTexts.listTitle = value;
    +  }
    +}
    +
    +// now we load our page
    +const page = await sp.web.loadClientsidePage("/sites/dev/SitePages/List-Web-Part.aspx");
    +
    +// get our part and pass it to the constructor of our wrapper class
    +const part = new ListWebpart(page.sections[0].columns[0].getControl(0));
    +
    +part.DisplayTitle = "My New Title!";
    +
    +await page.save();
    +
    +
    +

    Unfortunately each webpart can be authored differently, so there isn't a way to know how the setting for a given webpart are stored without loading it and examining the properties.

    +
    +

    Page Operations

    +

    There are other operation you can perform on a page in addition to manipulating the content.

    +

    pageLayout

    +

    You can get and set the page layout. Changing the layout after creating the page may have side effects and should be done cautiously.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.pageLayout;
    +
    +// set the value
    +page.pageLayout = "Article";
    +await page.save();
    +
    +

    bannerImageUrl

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.bannerImageUrl;
    +
    +// set the value
    +page.bannerImageUrl = "/server/relative/path/to/image.png";
    +await page.save();
    +
    +
    +

    Banner images need to exist within the same site collection as the page where you want to use them.

    +
    +

    thumbnailUrl

    +

    Allows you to set the thumbnail used for the page independently of the banner.

    +
    +

    If you set the bannerImageUrl property and not thumbnailUrl the thumbnail will be reset to match the banner, mimicking the UI functionality.

    +
    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.thumbnailUrl;
    +
    +// set the value
    +page.thumbnailUrl = "/server/relative/path/to/image.png";
    +await page.save();
    +
    +

    topicHeader

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.topicHeader;
    +
    +// set the value
    +page.topicHeader = "My cool header!";
    +await page.save();
    +
    +// clear the topic header and hide it
    +page.topicHeader = "";
    +await page.save();
    +
    +

    title

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.title;
    +
    +// set the value
    +page.title = "My page title";
    +await page.save();
    +
    +

    description

    +
    +

    Descriptions are limited to 255 chars

    +
    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.description;
    +
    +// set the value
    +page.description = "A description";
    +await page.save();
    +
    +

    layoutType

    +

    Sets the layout type of the page. The valid values are: "FullWidthImage", "NoImage", "ColorBlock", "CutInShape"

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.layoutType;
    +
    +// set the value
    +page.layoutType = "ColorBlock";
    +await page.save();
    +
    +

    headerTextAlignment

    +

    Sets the header text alignment to one of "Left" or "Center"

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.headerTextAlignment;
    +
    +// set the value
    +page.headerTextAlignment = "Center";
    +await page.save();
    +
    +

    showTopicHeader

    +

    Sets if the topic header is displayed on a page.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.showTopicHeader;
    +
    +// show the header
    +page.showTopicHeader = true;
    +await page.save();
    +
    +// hide the header
    +page.showTopicHeader = false;
    +await page.save();
    +
    +

    showPublishDate

    +

    Sets if the publish date is displayed on a page.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the current value
    +const value = page.showPublishDate;
    +
    +// show the date
    +page.showPublishDate = true;
    +await page.save();
    +
    +// hide the date
    +page.showPublishDate = false;
    +await page.save();
    +
    +

    Get / Set author details

    +

    Added in 2.0.4

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// get the author details (string | null)
    +const value = page.authorByLine;
    +
    +// set the author by user id
    +const user = await web.currentUser.select("Id", "LoginName")();
    +const userId = user.Id;
    +const userLogin = user.LoginName;
    +
    +await page.setAuthorById(userId);
    +await page.save();
    +
    +await page.setAuthorByLoginName(userLogin);
    +await page.save();
    +
    +
    +

    you must still save the page after setting the author to persist your changes as shown in the example.

    +
    +

    load

    +

    Loads the page from the server. This will overwrite any local unsaved changes.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +await page.load();
    +
    +

    save

    +

    Saves any changes to the page, optionally keeping them in draft state.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// changes are published
    +await page.save();
    +
    +// changes remain in draft
    +await page.save(false);
    +
    +

    discardPageCheckout

    +

    Discards any current checkout of the page by the current user.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +await page.discardPageCheckout();
    +
    +

    promoteToNews

    +

    Promotes the page as a news article.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +await page.promoteToNews();
    +
    +

    enableComments & disableComments

    +

    Used to control the availability of comments on a page.

    +

    Known Issue Banner

    +
    // you need to import the comments sub-module or use the all preset
    +import "@pnp/sp/comments/clientside-page";
    +
    +// our page instance
    +const page: IClientsidePage;
    +
    +// turn on comments
    +await page.enableComments();
    +
    +// turn off comments
    +await page.disableComments();
    +
    +

    findControlById

    +

    Finds a control within the page by id.

    +
    import { ClientsideText } from "@pnp/sp/clientside-pages";
    +
    +// our page instance
    +const page: IClientsidePage;
    +
    +const control = page.findControlById("06d4cdf6-bce6-4200-8b93-667a1b0a6c9d");
    +
    +// you can also type the control
    +const control = page.findControlById<ClientsideText>("06d4cdf6-bce6-4200-8b93-667a1b0a6c9d");
    +
    +

    findControl

    +

    Finds a control within the page using the supplied delegate. Can also be used to iterate through all controls in the page.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// find the first control whose order is 9
    +const control = page.findControl((c) => c.order === 9);
    +
    +// iterate all the controls and output the id to the console
    +page.findControl((c) => {
    +    console.log(c.id);
    +    return false;
    +});
    +
    +

    like & unlike

    +

    Updates the page's like value for the current user.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// like this page
    +await page.like();
    +
    +// unlike this page
    +await page.unlike();
    +
    +

    getLikedByInformation

    +

    Gets the likes information for this page.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +const info = await page.getLikedByInformation();
    +
    +

    copy

    +

    Creates a copy of the page, including all controls.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// our page instance
    +const page: IClientsidePage;
    +
    +// creates a published copy of the page
    +const pageCopy = await page.copy(sp.web, "newpagename", "New Page Title");
    +
    +// creates a draft (unpublished) copy of the page
    +const pageCopy2 = await page.copy(sp.web, "newpagename", "New Page Title", false);
    +
    +// edits to pageCopy2 ...
    +
    +// publish the page
    +pageCopy2.save();
    +
    +

    copyTo

    +

    Copies the contents of a page to another existing page instance.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// our page instances, loaded in any of the ways shown above
    +const source: IClientsidePage;
    +const target: IClientsidePage;
    +const target2: IClientsidePage;
    +
    +// creates a published copy of the page
    +await source.copyTo(target);
    +
    +// creates a draft (unpublished) copy of the page
    +await source.copyTo(target2, false);
    +
    +// edits to target2...
    +
    +// publish the page
    +target2.save();
    +
    +

    setBannerImage

    +

    Sets the banner image url and optionally additional properties. Allows you to set additional properties if needed, if you do not need to set the additional properties they are equivalent.

    +
    +

    Banner images need to exist within the same site collection as the page where you want to use them.

    +
    +
    // our page instance
    +const page: IClientsidePage;
    +
    +page.setBannerImage("/server/relative/path/to/image.png");
    +
    +// save the changes
    +await page.save();
    +
    +// set additional props
    +page.setBannerImage("/server/relative/path/to/image.png", {
    +    altText: "Image description",
    +    imageSourceType: 2,
    +    translateX: 30,
    +    translateY: 1234,
    +});
    +
    +// save the changes
    +await page.save();
    +
    +

    This sample shows the full process of adding a page, image file, and setting the banner image in nodejs. The same code would work in a browser with an update on how you get the file - likely from a file input or similar.

    +
    import { SPFetchClient } from "@pnp/nodejs";
    +import { join } from "path";
    +import { readFileSync } from "fs";
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/clientside-pages";
    +
    +// configure your node options
    +sp.setup({
    +  sp: {
    +    fetchClientFactory: () => {
    +      return new SPFetchClient("{Site Url}", "{Client Id}", "{Client Secret}");
    +    },
    +  },
    +});
    +
    +// add the banner image
    +const dirname = join("C:/path/to/file", "img-file.jpg");
    +const file: Uint8Array = new Uint8Array(readFileSync(dirname));
    +const far = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents").files.add("banner.jpg", file, true);
    +
    +// add the page
    +const page = await sp.web.addClientsidePage("MyPage", "Page Title");
    +
    +// set the banner image
    +page.setBannerImage(far.data.ServerRelativeUrl);
    +
    +// publish the page
    +await page.save();
    +
    +

    setBannerImageFromExternalUrl

    +

    Added in 2.0.12

    +

    Allows you to set the banner image from a source outside the current site collection. The image file will be copied to the SiteAssets library and referenced from there.

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// you must await this method
    +await page.setBannerImageFromExternalUrl("https://absolute.url/to/my/image.jpg");
    +
    +// save the changes
    +await page.save();
    +
    +

    You can optionally supply additional props for the banner image, these match the properties when calling setBannerImage

    +
    // our page instance
    +const page: IClientsidePage;
    +
    +// you must await this method
    +await page.setBannerImageFromExternalUrl("https://absolute.url/to/my/image.jpg", {
    +    altText: "Image description",
    +    imageSourceType: 2,
    +    translateX: 30,
    +    translateY: 1234,
    +});
    +
    +// save the changes
    +await page.save();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/column-defaults/index.html b/v2/sp/column-defaults/index.html new file mode 100644 index 000000000..df2fb8371 --- /dev/null +++ b/v2/sp/column-defaults/index.html @@ -0,0 +1,2540 @@ + + + + + + + + + + + + + + + + + + Column Defaults - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/column-defaults

    +

    The column defaults sub-module allows you to manage the default column values on a library or library folder.

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from "@pnp/sp/column-defaults";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/column-defaults";
    Preset: Allimport { sp, IFieldDefault, IFieldDefaultProps, AllowedDefaultColumnValues } from "@pnp/sp/presents/all";
    +

    Get Folder Defaults

    +

    You can get the default values for a specific folder as shown below:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders/web";
    +import "@pnp/sp/column-defaults";
    +
    +const defaults = await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").getDefaultColumnValues();
    +
    +/*
    +The resulting structure will have the form:
    +
    +[
    +  {
    +    "name": "{field internal name}",
    +    "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
    +    "value": "{the default value}"
    +  },
    +  {
    +    "name": "{field internal name}",
    +    "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
    +    "value": "{the default value}"
    +  }
    +]
    +*/
    +
    +

    Set Folder Defaults

    +

    When setting the defaults for a folder you need to include the field's internal name and the value.

    +
    +

    For more examples of other field types see the section Pattern for setting defaults on various column types

    +

    Note: Be very careful when setting the path as the site collection url is case sensitive

    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders/web";
    +import "@pnp/sp/column-defaults";
    +
    +await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").setDefaultColumnValues([{
    +  name: "TextField",
    +  value: "Something",
    +},
    +{
    +  name: "NumberField",
    +  value: 14,
    +}]);
    +
    +

    Get Library Defaults

    +

    You can also get all of the defaults for the entire library.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/column-defaults";
    +
    +const defaults = await sp.web.lists.getByTitle("DefaultColumnValues").getDefaultColumnValues();
    +
    +/*
    +The resulting structure will have the form:
    +
    +[
    +  {
    +    "name": "{field internal name}",
    +    "path": "/sites/dev/DefaultColumnValues",
    +    "value": "{the default value}"
    +  },
    +  {
    +    "name": "{field internal name}",
    +    "path": "/sites/dev/DefaultColumnValues/fld_GHk5",
    +    "value": "{a different default value}"
    +  }
    +]
    +*/
    +
    +

    Set Library Defaults

    +

    You can also set the defaults for an entire library at once (root and all sub-folders). This may be helpful in provisioning a library or other scenarios. When setting the defaults for the entire library you must also include the path value with is the server relative path to the folder. When setting the defaults for a folder you need to include the field's internal name and the value.

    +
    +

    For more examples of other field types see the section Pattern for setting defaults on various column types

    +

    Note: Be very careful when setting the path as the site collection url is case sensitive

    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/column-defaults";
    +
    +await sp.web.lists.getByTitle("DefaultColumnValues").setDefaultColumnValues([{
    +                name: "TextField",
    +                path: "/sites/dev/DefaultColumnValues",
    +                value: "#PnPjs Rocks!",
    +            }]);
    +
    +

    Clear Folder Defaults

    +

    If you want to clear all of the folder defaults you can use the clear method:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders/web";
    +import "@pnp/sp/column-defaults";
    +
    +await sp.web.getFolderByServerRelativePath("/sites/dev/DefaultColumnValues/fld_GHk5").clearDefaultColumnValues();
    +
    +

    Clear Library Defaults

    +

    If you need to clear all of the default column values in a library you can pass an empty array to the list's setDefaultColumnValues method.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/column-defaults";
    +
    +await sp.web.lists.getByTitle("DefaultColumnValues").setDefaultColumnValues([]);
    +
    +

    Pattern for setting defaults on various column types

    +

    The following is an example of the structure for setting the default column value when using the setDefaultColumnValues that covers the various field types.

    +
    [{
    +    // Text/Boolean/CurrencyDateTime/Choice/User
    +    name: "TextField":
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: "#PnPjs Rocks!",
    +}, {
    +    //Number
    +    name: "NumberField",
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: 42,
    +}, {
    +    //Date
    +    name: "NumberField",
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: "1900-01-01T00:00:00Z",
    +}, {
    +    //Date - Today
    +    name: "NumberField",
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: "[today]",
    +}, {
    +    //MultiChoice
    +    name: "MultiChoiceField",
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: ["Item 1", "Item 2"],
    +}, {
    +    //MultiChoice - single value
    +    name: "MultiChoiceField",
    +    path: "/sites/dev/DefaultColumnValues/folder2",
    +    value: ["Item 1"],
    +}, {
    +    //Taxonomy - single value
    +    name: "TaxonomyField",
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: {
    +        wssId:"-1",
    +        termName: "TaxValueName",
    +        termId: "924d2077-d5e3-4507-9f36-4a3655e74274"
    +        }
    +}, {
    +    //Taxonomy - multiple value
    +    name: "TaxonomyMultiField",
    +    path: "/sites/dev/DefaultColumnValues",
    +    value: [{
    +        wssId:"-1",
    +        termName: "TaxValueName",
    +        termId: "924d2077-d5e3-4507-9f36-4a3655e74274"
    +        },{
    +        wssId:"-1",
    +        termName: "TaxValueName2",
    +        termId: "95d4c307-dde5-49d8-b861-392e145d94d3"
    +        },]
    +}]);
    +
    +

    Taxonomy Full Example

    +

    This example shows fully how to get the taxonomy values and set them as a default column value using PnPjs.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/column-defaults";
    +import "@pnp/sp/taxonomy";
    +
    +// get the term's info we want to use as the default
    +const term = await sp.termStore.sets.getById("ea6fc521-d293-4f3d-9e84-f3a5bc0936ce").getTermById("775c9cf6-c3cd-4db9-8cfa-fc0aeefad93a")();
    +
    +// get the default term label
    +const defLabel = term.labels.find(v => v.isDefault);
    +
    +// set the default value using -1, the term id, and the term's default label name
    +await sp.web.lists.getByTitle("MetaDataDocLib").rootFolder.setDefaultColumnValues([{
    +    name: "MetaDataColumnInternalName",
    +    value: {
    +        wssId: "-1",
    +        termId: term.id,
    +        termName: defLabel.name,
    +    }
    +}])
    +
    +// check that the defaults have updated
    +const newDefaults = await sp.web.lists.getByTitle("MetaDataDocLib").getDefaultColumnValues();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/comments-likes/index.html b/v2/sp/comments-likes/index.html new file mode 100644 index 000000000..ff6e0c55f --- /dev/null +++ b/v2/sp/comments-likes/index.html @@ -0,0 +1,2646 @@ + + + + + + + + + + + + + + + + + + Comments and Likes - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    + +
    + + +
    +
    + + + + + + + +

    @pnp/sp/comments and likes

    +

    Comments can be accessed through either IItem or IClientsidePage instances, though in slightly different ways. For information on loading clientside pages or items please refer to those articles.

    +

    These APIs are currently in BETA and are subject to change or may not work on all tenants.

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/comments";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    ClientsidePage Comments

    +

    The IClientsidePage interface has three methods to provide easier access to the comments for a page, without requiring that you load the item separately.

    +

    Add Comments

    +

    You can add a comment using the addComment method as shown

    +
    import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
    +import "@pnp/sp/comments/clientside-page";
    +
    +const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
    +// optionally publish the page first
    +await page.save();
    +
    +const comment = await page.addComment("A test comment");
    +
    +

    Get Page Comments

    +
    import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
    +import "@pnp/sp/comments/clientside-page";
    +
    +const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
    +// optionally publish the page first
    +await page.save();
    +
    +await page.addComment("A test comment");
    +await page.addComment("A test comment");
    +await page.addComment("A test comment");
    +await page.addComment("A test comment");
    +await page.addComment("A test comment");
    +await page.addComment("A test comment");
    +
    +const comments = await page.getComments();
    +
    +

    enableComments & disableComments

    +

    Used to control the availability of comments on a page

    +
    // you need to import the comments sub-module or use the all preset
    +import "@pnp/sp/comments/clientside-page";
    +
    +// our page instance
    +const page: IClientsidePage;
    +
    +// turn on comments
    +await page.enableComments();
    +
    +// turn off comments
    +await page.disableComments();
    +
    +

    GetById

    +
    import { CreateClientsidePage } from "@pnp/sp/clientside-pages";
    +import "@pnp/sp/comments/clientside-page";
    +
    +const page = await CreateClientsidePage(sp.web, "mypage", "My Page Title", "Article");
    +// optionally publish the page first
    +await page.save();
    +
    +const comment = await page.addComment("A test comment");
    +
    +const commentData = await page.getCommentById(parseInt(comment.id, 10));
    +
    +

    Clear Comments

    +

    Item Comments

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files/web";
    +import "@pnp/sp/items";
    +import "@pnp/sp/comments/item";
    +
    +const item = await sp.web.getFileByServerRelativeUrl("/sites/dev/SitePages/Test_8q5L.aspx").getItem();
    +
    +// as an example, or any of the below options
    +await item.like();
    +
    +

    The below examples use a variable named "item" which is taken to represent an IItem instance.

    +

    Comments

    +

    Get Item Comments

    +
    const comments = await item.comments();
    +
    +

    You can also get the comments merged with instances of the Comment class to immediately start accessing the properties and methods:

    +
    import { spODataEntityArray } from "@pnp/sp/odata";
    +import { Comment, ICommentData } from "@pnp/sp/comments";
    +
    +const comments = await item.comments(spODataEntityArray<Comment, CommentData>(Comment));
    +
    +// these will be Comment instances in the array
    +comments[0].replies.add({ text: "#PnPjs is pretty ok!" });
    +
    +//load the top 20 replies and comments for an item including likedBy information
    +const comments = await item.comments.expand("replies", "likedBy", "replies/likedBy").top(20)();
    +
    +

    Add Comment

    +
    // you can add a comment as a string
    +item.comments.add("string comment");
    +
    +// or you can add it as an object to include mentions
    +item.comments.add({ text: "comment from object property" });
    +
    +

    Delete a Comment

    +
    import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
    +
    +const comments = await item.comments(spODataEntityArray<Comment, CommentData>(Comment));
    +
    +// these will be Comment instances in the array
    +comments[0].delete()
    +
    +

    Like Comment

    +
    import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
    +
    +const comments = await item.comments(spODataEntityArray<Comment, CommentData>(Comment));
    +
    +// these will be Comment instances in the array
    +comments[0].like()
    +
    +

    Unlike Comment

    +
    import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
    +
    +const comments = await item.comments(spODataEntityArray<Comment, CommentData>(Comment));
    +
    +comments[0].unlike()
    +
    +

    Reply to a Comment

    +
    import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
    +
    +const comments = await item.comments(spODataEntityArray<Comment, CommentData>(Comment));
    +
    +const comment: Comment & CommentData = await comments[0].replies.add({ text: "#PnPjs is pretty ok!" });
    +
    +

    Load Replies to a Comment

    +
    import { spODataEntityArray, Comment, CommentData } from "@pnp/sp";
    +
    +const comments = await item.comments(spODataEntityArray<Comment, CommentData>(Comment));
    +
    +const replies = await comments[0].replies();
    +
    +

    Like

    +

    You can like/unlike client-side pages, items, and comments on items. See above for how to like or unlike a comment. Below you can see how to like and unlike an items, as well as get the liked by data.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/comments/item";
    +import { ILikeData, ILikedByInformation } from "@pnp/sp/comments";
    +
    +// like an item
    +await item.like();
    +
    +// unlike an item
    +await item.unlike();
    +
    +// get the liked by data
    +const likedByData: ILikeData[] = await item.getLikedBy();
    +
    +// get the liked by information
    +const likedByInfo: ILikedByInformation = await item.getLikedByInformation();
    +
    +

    To like/unlike a client-side page and get liked by information.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/comments/clientside-page";
    +import { ILikedByInformation } from "@pnp/sp/comments";
    +
    +// like a page
    +await page.like();
    +
    +// unlike a page
    +await page.unlike();
    +
    +// get the liked by information
    +const likedByInfo: ILikedByInformation = await page.getLikedByInformation();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/content-types/index.html b/v2/sp/content-types/index.html new file mode 100644 index 000000000..9bb4d4b1b --- /dev/null +++ b/v2/sp/content-types/index.html @@ -0,0 +1,2462 @@ + + + + + + + + + + + + + + + + + + Content Types - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/content-types

    +

    Content Types are used to define sets of columns in SharePoint.

    +

    IContentTypes

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Webs, IWebs } from "@pnp/sp/webs";
    import { ContentTypes, IContentTypes } from "@pnp/sp/content-types";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/content-types";
    Preset: Allimport { sp, ContentTypes, IContentTypes } from "@pnp/sp/presets/all";
    +

    Add an existing Content Type to a collection

    +

    The following example shows how to add the built in Picture Content Type to the Documents library.

    +
    sp.web.lists.getByTitle("Documents").contentTypes.addAvailableContentType("0x010102");
    +
    +

    Get a Content Type by Id

    +
    const d: IContentType = await sp.web.contentTypes.getById("0x01")();
    +
    +// log content type name to console
    +console.log(d.name);
    +
    +

    Add a new Content Type

    +

    To add a new Content Type to a collection, parameters id and name are required. For more information on creating content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.

    +
    sp.web.contentTypes.add("0x01008D19F38845B0884EBEBE239FDF359184", "My Content Type");
    +
    +

    It is also possible to provide a description and group parameter. For other settings, we can use the parameter named 'additionalSettings' which is a TypedHash, meaning you can send whatever properties you'd like in the body (provided that the property is supported by the SharePoint API).

    +
    //Adding a content type with id, name, description, group and setting it to read only mode (using additionalsettings)
    +sp.web.contentTypes.add("0x01008D19F38845B0884EBEBE239FDF359184", "My Content Type", "This is my content type.", "_PnP Content Types", { ReadOnly: true });
    +
    +

    IContentType

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { ContentType, IContentType } from "@pnp/sp/content-types";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/content-types";
    Preset: Allimport { sp, ContentType, IContentType } from "@pnp/sp/presets/all";
    +

    Invokable Banner Selective Imports Banner

    + +

    Use this method to get a collection containing all the field links (SP.FieldLink) for a Content Type.

    +
    // get field links from built in Content Type Document (Id: "0x0101")
    +const d = await sp.web.contentTypes.getById("0x0101").fieldLinks();
    +
    +// log collection of fieldlinks to console
    +console.log(d);
    +
    +

    Get Content Type fields

    +

    To get a collection with all fields on the Content Type, simply use this method.

    +
    // get fields from built in Content Type Document (Id: "0x0101")
    +const d = await sp.web.contentTypes.getById("0x0101").fields();
    +
    +// log collection of fields to console
    +console.log(d);
    +
    +

    Get parent Content Type

    +
    // get parent Content Type from built in Content Type Document (Id: "0x0101")
    +const d = await sp.web.contentTypes.getById("0x0101").parent();
    +
    +// log name of parent Content Type to console
    +console.log(d.Name)
    +
    +

    Get Content Type Workflow associations

    +
    // get workflow associations from built in Content Type Document (Id: "0x0101")
    +const d = await sp.web.contentTypes.getById("0x0101").workflowAssociations();
    +
    +// log collection of workflow associations to console
    +console.log(d);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/custom-irequestclient/index.html b/v2/sp/custom-irequestclient/index.html new file mode 100644 index 000000000..b6258e94f --- /dev/null +++ b/v2/sp/custom-irequestclient/index.html @@ -0,0 +1,2339 @@ + + + + + + + + + + + + + + + + + + Custom Request Client - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    Custom IRequestClient

    +

    Scenario: You have some special requirements involving auth scenarios or other needs that the library can't directly support. You may need to create a custom IRequestClient implementation to meet those needs as we can't customize the library to handle every case. This article walks you through how to create a custom IRequestClient and register it for use by the library.

    +
    +

    It is very unlikely this is a step you ever need to take and we encourage you to ask a question in the issues list before going down this path.

    +
    +

    Create the Client

    +

    The easiest way to create a new IRequestClient is to subclass the existing SPHttpClient. You can always write a full client from scratch so long as it supports the IRequestClient interface but you need to handle all of the logic for retry, headers, and the request digest.

    +

    Here we show implementing a client to solve the need discussed in pull request 1264 as an example.

    +
    // we subclass SPHttpClient
    +class CustomSPHttpClient extends SPHttpClient {
    +
    +  // optionally add a constructor, done here as an example
    +  constructor(impl?: IHttpClientImpl) {
    +    super(impl);
    +  }
    +
    +  // override the fetchRaw method to ensure we always include the credentials = "include" option
    +  // you could also override fetch, but fetchRaw ensures no matter what all requests get your custom logic is applied
    +  public fetchRaw(url: string, options?: IFetchOptions): Promise<Response> {
    +    options.credentials = "include";
    +    return super.fetchRaw(url, options);
    +  }
    +}
    +
    +

    The final step is to register the custom client with the library so it is used instead of the default. For that we import the registerCustomRequestClientFactory function and call it before our request generating code. You can reset to the default client factory by passing null to this same function.

    +
    import { sp, registerCustomRequestClientFactory } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +registerCustomRequestClientFactory(() => new CustomSPHttpClient());
    +
    +// configure your other options
    +sp.setup({
    +    // ...
    +});
    +
    +// this request will be executed through your custom client
    +const w = await sp.web();
    +
    +

    Unregister Custom Client

    +
    // unregister custom client factory
    +registerCustomRequestClientFactory(null);
    +
    +

    IRequestClient Interface

    +

    If you want to 100% roll your own client you need to implement the below interface, found in common.

    +
    import { IRequestClient } from "@pnp/core";
    +
    +
    export interface IRequestClient {
    +    fetch(url: string, options?: IFetchOptions): Promise<Response>;
    +    fetchRaw(url: string, options?: IFetchOptions): Promise<Response>;
    +    get(url: string, options?: IFetchOptions): Promise<Response>;
    +    post(url: string, options?: IFetchOptions): Promise<Response>;
    +    patch(url: string, options?: IFetchOptions): Promise<Response>;
    +    delete(url: string, options?: IFetchOptions): Promise<Response>;
    +}
    +
    +

    Supportability Note

    +

    We cannot provide support for your custom client implementation, and creating your own client assumes an intimate knowledge of how SharePoint requests work. Again, this is very likely something you will never need to do - and we recommend exhausting all other options before taking this route.

    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/entity-merging/index.html b/v2/sp/entity-merging/index.html new file mode 100644 index 000000000..89363ad03 --- /dev/null +++ b/v2/sp/entity-merging/index.html @@ -0,0 +1,2325 @@ + + + + + + + + + + + + + + + + + + Entity Merging - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp - entity merging

    +

    Sometimes when we make a query entity's data we would like then to immediately run other commands on the returned entity. To have data returned as its representing type we make use of the spODataEntity and spODataEntityArray parsers. The below approach works for all instance types such as List, Web, Item, or Field as examples.

    +

    Importing spODataEntity and spODataEntityArray

    +

    You can import spODataEntity and spODataEntityArray in two ways, depending on your use case. The simplest way is to use the presets/all import as shown in the examples. The downside of this approach is that you can't take advantage of selective imports.

    +

    If you want to take advantage of selective imports while using either of the entity parsers you can use:

    +
    import { spODataEntity, spODataEntityArray } from "@pnp/sp/odata";
    +
    +

    The full selective import for the first sample would be:

    +
    import { sp } from "@pnp/sp";
    +import { spODataEntity } from "@pnp/sp/odata";
    +import { Item, IItem } from "@pnp/sp/items";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +
    +

    Request a single entity

    +

    If we are loading a single entity we use the spODataEntity method. Here we show loading a list item using the Item class and a simple get query.

    +
    import { sp, spODataEntity, Item, IItem } from "@pnp/sp/presets/all";
    +
    +// interface defining the returned properties
    +interface MyProps {
    +    Id: number;
    +}
    +
    +try {
    +
    +    // get a list item loaded with data and merged into an instance of Item
    +    const item = await sp.web.lists.getByTitle("ListTitle").items.getById(1).usingParser(spODataEntity<IItem, MyProps>(Item))();
    +
    +    // log the item id, all properties specified in MyProps will be type checked
    +    Logger.write(`Item id: ${item.Id}`);
    +
    +    // now we can call update because we have an instance of the Item type to work with as well
    +    await item.update({
    +        Title: "New title.",
    +    });
    +
    +} catch (e) {
    +    Logger.error(e);
    +}
    +
    +

    Request a collection

    +

    The same pattern works when requesting a collection of objects with the exception of using the spODataEntityArray method.

    +
    import { sp, spODataEntityArray, Item, IItem } from "@pnp/sp/presets/all";
    +
    +// interface defining the returned properties
    +interface MyProps {
    +    Id: number;
    +    Title: string;
    +}
    +
    +try {
    +
    +    // get a list item loaded with data and merged into an instance of Item
    +    const items = await sp.web.lists.getByTitle("OrderByList").items.select("Id", "Title").usingParser(spODataEntityArray<IItem, MyProps>(Item))();
    +
    +    Logger.write(`Item id: ${items.length}`);
    +
    +    Logger.write(`Item id: ${items[0].Title}`);
    +
    +    // now we can call update because we have an instance of the Item type to work with as well
    +    await items[0].update({
    +        Title: "New title.",
    +    });
    +
    +} catch (e) {
    +
    +    Logger.error(e);
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/features/index.html b/v2/sp/features/index.html new file mode 100644 index 000000000..784a4ff7b --- /dev/null +++ b/v2/sp/features/index.html @@ -0,0 +1,2440 @@ + + + + + + + + + + + + + + + + + + Features - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/features

    +

    Features module provides method to get the details of activated features. And to activate/deactivate features scoped at Site Collection and Web.

    +

    IFeatures

    +

    Invokable Banner Selective Imports Banner

    +

    Represents a collection of features. SharePoint Sites and Webs will have a collection of features

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/features/site";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/features/web";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/features";
    Selective 4import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/features";
    Preset: Allimport { sp, IFeatures, Features } from "@pnp/sp/presets/all";
    +

    getById

    +

    Gets the information about a feature for the given GUID

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/features";
    +
    +//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
    +const webFeatureId = "guid-of-web-feature";
    +const webFeature = await sp.web.features.getById(webFeatureId)();
    +
    +const siteFeatureId = "guid-of-site-scope-feature";
    +const siteFeature = await sp.site.features.getById(siteFeatureId)();
    +
    +

    add

    +

    Adds (activates) a feature at the Site or Web level

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/features";
    +
    +//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
    +const webFeatureId = "guid-of-web-feature";
    +let res = await sp.web.features.add(webFeatureId);
    +// Activate with force
    +res = await sp.web.features.add(webFeatureId, true);
    +
    +

    remove

    +

    Removes and deactivates the specified feature from the SharePoint Site or Web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/features";
    +
    +//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
    +const webFeatureId = "guid-of-web-feature";
    +let res = await sp.web.features.remove(webFeatureId);
    +// Deactivate with force
    +res = await sp.web.features.remove(webFeatureId, true);
    +
    +

    IFeature

    +

    Represents an instance of a SharePoint feature.

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/features/site";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/features/web";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/features";
    Selective 4import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/features";
    Preset: Allimport { sp, IFeatures, Features, IFeature, Feature } from "@pnp/sp/presets/all";
    +

    deactivate

    +

    Deactivates the specified feature from the SharePoint Site or Web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/features";
    +
    +//Example of GUID format a7a2793e-67cd-4dc1-9fd0-43f61581207a
    +const webFeatureId = "guid-of-web-feature";
    +sp.web.features.getById(webFeatureId).deactivate()
    +
    +// Deactivate with force
    +sp.web.features.getById(webFeatureId).deactivate(true)
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/fields/index.html b/v2/sp/fields/index.html new file mode 100644 index 000000000..cf538cc9e --- /dev/null +++ b/v2/sp/fields/index.html @@ -0,0 +1,3103 @@ + + + + + + + + + + + + + + + + + + Fields - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/lists

    +

    Fields in SharePoint can be applied to both webs and lists. When referencing a webs' fields you are effectively looking at site columns which are common fields that can be utilized in any list/library in the site. When referencing a lists' fields you are looking at the fields only associated to that particular list.

    +

    IFields

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Webs, IWebs } from "@pnp/sp/webs";
    import { Fields, IFields } from "@pnp/sp/fields";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/fields";
    Preset: Allimport { sp, Fields, IFields } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp, Fields, IFields } from "@pnp/sp/presets/core";
    +

    Get Field by Id

    +

    Gets a field from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

    +
    import { sp } from "@pnp/sp";
    +import { IField } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/fields";
    +
    +// get the field by Id for web
    +const field: IField = sp.web.fields.getById("03b05ff4-d95d-45ed-841d-3855f77a2483");
    +// get the field by Id for list 'My List'
    +const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getById("03b05ff4-d95d-45ed-841d-3855f77a2483")();
    +
    +// we can use this 'field' variable to execute more queries on the field:
    +const r = await field.select("Title")();
    +
    +// show the response from the server
    +console.log(r.Title);
    +
    +

    Get Field by Title

    +

    You can also get a field from the collection by title.

    +
    import { sp } from "@pnp/sp";
    +import { IField } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists"
    +import "@pnp/sp/fields";
    +
    +// get the field with the title 'Author' for web
    +const field: IField = sp.web.fields.getByTitle("Author");
    +// get the field with the title 'Author' for list 'My List'
    +const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getByTitle("Author")();
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Get Field by Internal Name or Title

    +

    You can also get a field from the collection regardless of if the string is the fields internal name or title which can be different.

    +
    import { sp } from "@pnp/sp";
    +import { IField } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists"
    +import "@pnp/sp/fields";
    +
    +// get the field with the internal name 'ModifiedBy' for web
    +const field: IField = sp.web.fields.getByInternalNameOrTitle("ModifiedBy");
    +// get the field with the internal name 'ModifiedBy' for list 'My List'
    +const field2: IFieldInfo = await sp.web.lists.getByTitle("My List").fields.getByInternalNameOrTitle("ModifiedBy")();
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Create a Field using an XML schema

    +

    Create a new field by defining an XML schema that assigns all the properties for the field.

    +
    import { sp } from "@pnp/sp";
    +import { IField } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// define the schema for your new field, in this case a date field with a default date of today.
    +const fieldSchema = `<Field ID="{03b09ff4-d99d-45ed-841d-3855f77a2483}" StaticName="MyField" Name="MyField" DisplayName="My New Field" FriendlyDisplayFormat="Disabled" Format="DateOnly" Type="DateTime" Group="My Group"><Default>[today]</Default></Field>`;
    +
    +// create the new field in the web
    +const field: IFieldAddResult = await sp.web.fields.createFieldAsXml(fieldSchema);
    +// create the new field in the list 'My List'
    +const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(fieldSchema);
    +
    +// we can use this 'field' variable to run more queries on the list:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a New Field

    +

    Use the add method to create a new field where you define the field type

    +
    import { sp } from "@pnp/sp";
    +import { IField } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new field called 'My Field' in web.
    +const field: IFieldAddResult = await sp.web.fields.add("My Field", "SP.FieldText", { FieldTypeKind: 3, Group: "My Group" });
    +// create a new field called 'My Field' in the list 'My List'
    +const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.add("My Field", "SP.FieldText", { FieldTypeKind: 3, Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Site Field to a List

    +

    Use the createFieldAsXml method to add a site field to a list.

    +
    import { sp } from "@pnp/sp";
    +import { IFieldAddResult } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new field called 'My Field' in web.
    +const field: IFieldAddResult = await sp.web.fields.add("My Field", "SP.FieldText", { FieldTypeKind: 3, Group: "My Group" });
    +// add the site field 'My Field' to the list 'My List'
    +const r = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(field.data.SchemaXml);
    +
    +// log the field Id to console
    +console.log(r.data.Id);
    +
    +

    Add a Text Field

    +

    Use the addText method to create a new text field.

    +
    import { sp } from "@pnp/sp";
    +import { IFieldAddResult } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new text field called 'My Field' in web.
    +const field: IFieldAddResult = await sp.web.fields.addText("My Field", 255, { Group: "My Group" });
    +// create a new text field called 'My Field' in the list 'My List'.
    +const field2: IFieldAddResult = await sp.web.lists.getByTitle("My List").fields.addText("My Field", 255, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Calculated Field

    +

    Use the addCalculated method to create a new calculated field.

    +
    import { sp } from "@pnp/sp";
    +import { DateTimeFieldFormatType, FieldTypes } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new calculated field called 'My Field' in web
    +const field = await sp.web.fields.addCalculated("My Field", "=Modified+1", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: "MyGroup" });
    +// create a new calculated field called 'My Field' in the list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addCalculated("My Field", "=Modified+1", DateTimeFieldFormatType.DateOnly, FieldTypes.DateTime, { Group: "MyGroup" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Date/Time Field

    +

    Use the addDateTime method to create a new date/time field.

    +
    import { sp } from "@pnp/sp";
    +import { DateTimeFieldFormatType, CalendarType, DateTimeFieldFriendlyFormatType } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new date/time field called 'My Field' in web
    +const field = await sp.web.fields.addDateTime("My Field", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: "My Group" });
    +// create a new date/time field called 'My Field' in the list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addDateTime("My Field", DateTimeFieldFormatType.DateOnly, CalendarType.Gregorian, DateTimeFieldFriendlyFormatType.Disabled, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Currency Field

    +

    Use the addCurrency method to create a new currency field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new currency field called 'My Field' in web
    +const field = await sp.web.fields.addCurrency("My Field", 0, 100, 1033, { Group: "My Group" });
    +// create a new currency field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addCurrency("My Field", 0, 100, 1033, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Multi-line Text Field

    +

    Use the addMultilineText method to create a new multi-line text field.

    +
    +

    For Enhanced Rich Text mode, see the next section.

    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new multi-line text field called 'My Field' in web
    +const field = await sp.web.fields.addMultilineText("My Field", 6, true, false, false, true, { Group: "My Group" });
    +// create a new multi-line text field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addMultilineText("My Field", 6, true, false, false, true, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Multi-line Text Field with Enhanced Rich Text

    +

    The REST endpoint doesn't support setting the RichTextMode field therefore you will need to revert to Xml to create the field. The following is an example that will create a multi-line text field in Enhanced Rich Text mode.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +//Create a new multi-line text field called 'My Field' in web
    +const field = await sp.web.lists.getByTitle("My List").fields.createFieldAsXml(
    +    `<Field Type="Note" Name="MyField" DisplayName="My Field" Required="FALSE" RichText="TRUE" RichTextMode="FullHtml" />`
    +);
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Number Field

    +

    Use the addNumber method to create a new number field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new number field called 'My Field' in web
    +const field = await sp.web.fields.addNumber("My Field", 1, 100, { Group: "My Group" });
    +// create a new number field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addNumber("My Field", 1, 100, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a URL Field

    +

    Use the addUrl method to create a new url field.

    +
    import { sp } from "@pnp/sp";
    +import { UrlFieldFormatType } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new url field called 'My Field' in web
    +const field = await sp.web.fields.addUrl("My Field", UrlFieldFormatType.Hyperlink, { Group: "My Group" });
    +// create a new url field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addUrl("My Field", UrlFieldFormatType.Hyperlink, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a User Field

    +

    Use the addUser method to create a new user field.

    +
    import { sp } from "@pnp/sp";
    +import { FieldUserSelectionMode } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new user field called 'My Field' in web
    +const field = await sp.web.fields.addUser("My Field", FieldUserSelectionMode.PeopleOnly, { Group: "My Group" });
    +// create a new user field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addUser("My Field", FieldUserSelectionMode.PeopleOnly, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Lookup Field

    +

    Use the addLookup method to create a new lookup field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +const list = await sp.web.lists.getByTitle("My Lookup List")();
    +// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in web.
    +const field = await sp.web.fields.addLookup("My Field", list.Id, "Title",  { Group: "My Group" });
    +// create a new lookup field called 'My Field' based on an existing list 'My Lookup List' showing 'Title' field in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addLookup("My Field", list.Id, "Title",  { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +// **
    +// Adding a lookup that supports multiple values takes two calls:
    +const fieldAddResult = await sp.web.fields.addLookup("Test Lookup 124", "GUID", "Title");
    +
    +await fieldAddResult.field.update({ Description: 'New Description' }, "SP.FieldLookup");
    +
    +

    Add a Choice Field

    +

    Use the addChoice method to create a new choice field.

    +
    import { sp } from "@pnp/sp";
    +import { ChoiceFieldFormatType } from "@pnp/sp/fields/types";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];
    +// create a new choice field called 'My Field' in web
    +const field = await sp.web.fields.addChoice("My Field", choices, ChoiceFieldFormatType.Dropdown, false, { Group: "My Group" });
    +// create a new choice field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addChoice("My Field", choices, ChoiceFieldFormatType.Dropdown, false, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Multi-Choice Field

    +

    Use the addMultiChoice method to create a new multi-choice field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +const choices = [`ChoiceA`, `ChoiceB`, `ChoiceC`];
    +// create a new multi-choice field called 'My Field' in web
    +const field = await sp.web.fields.addMultiChoice("My Field", choices, false, { Group: "My Group" });
    +// create a new multi-choice field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addMultiChoice("My Field", choices, false, { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Boolean Field

    +

    Use the addBoolean method to create a new boolean field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new boolean field called 'My Field' in web
    +const field = await sp.web.fields.addBoolean("My Field", { Group: "My Group" });
    +// create a new boolean field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addBoolean("My Field", { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Dependent Lookup Field

    +

    Use the addDependentLookupField method to create a new dependent lookup field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in web.
    +const field = await sp.web.fields.getByTitle("My Field")();
    +const fieldDep = await sp.web.fields.addDependentLookupField("My Dep Field", field.Id, "Description");
    +// create a new dependent lookup field called 'My Dep Field' showing 'Description' based on an existing 'My Field' lookup field in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field")();
    +const fieldDep2 = await sp.web.lists.getByTitle("My List").fields.addDependentLookupField("My Dep Field", field2.Id, "Description");
    +
    +// we can use this 'fieldDep' variable to run more queries on the field:
    +const r = await fieldDep.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Add a Location Field

    +

    Use the addLocation method to create a new location field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// create a new location field called 'My Field' in web
    +const field = await sp.web.fields.addLocation("My Field", { Group: "My Group" });
    +// create a new location field called 'My Field' in list 'My List'
    +const field2 = await sp.web.lists.getByTitle("My List").fields.addLocation("My Field", { Group: "My Group" });
    +
    +// we can use this 'field' variable to run more queries on the field:
    +const r = await field.field.select("Id")();
    +
    +// log the field Id to console
    +console.log(r.Id);
    +
    +

    Delete a Field

    +

    Use the delete method to delete a field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/fields";
    +
    +// delete one or more fields from web, returns boolean
    +const result = await sp.web.fields.getByTitle("My Field").delete();
    +const result2 = await sp.web.fields.getByTitle("My Field 2").delete();
    +// delete one or more fields from list 'My List', returns boolean
    +const result = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").delete();
    +const result2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field 2").delete();
    +
    +

    Update a Field

    +

    Use the update method to update a field.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// update the field called 'My Field' with a description in web, returns FieldUpdateResult
    +const fieldUpdate = await sp.web.fields.getByTitle("My Field").update({ Description: "My Description" });
    +// update the field called 'My Field' with a description in list 'My List', returns FieldUpdateResult
    +const fieldUpdate2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").update({ Description: "My Description" });
    +
    +// if you need to update a field with properties for a specific field type you can optionally include the field type as a second param
    +// if you do not include it we will look up the type, but that adds a call to the server
    +const fieldUpdate2 = await sp.web.lists.getByTitle("My List").fields.getByTitle("My Look up Field").update({ RelationshipDeleteBehavior: 1 }, "SP.FieldLookup");
    +
    +

    Show a Field in the Display Form

    +

    Use the setShowInDisplayForm method to add a field to the display form.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// show field called 'My Field' in display form throughout web
    +await sp.web.fields.getByTitle("My Field").setShowInDisplayForm(true);
    +// show field called 'My Field' in display form for list 'My List'
    +await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInDisplayForm(true);
    +
    +

    Show a Field in the Edit Form

    +

    Use the setShowInEditForm method to add a field to the edit form.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// show field called 'My Field' in edit form throughout web
    +await sp.web.fields.getByTitle("My Field").setShowInEditForm(true);
    +// show field called 'My Field' in edit form for list 'My List'
    +await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInEditForm(true);
    +
    +

    Show a Field in the New Form

    +

    Use the setShowInNewForm method to add a field to the display form.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/fields";
    +
    +// show field called 'My Field' in new form throughout web
    +await sp.web.fields.getByTitle("My Field").setShowInNewForm(true);
    +// show field called 'My Field' in new form for list 'My List'
    +await sp.web.lists.getByTitle("My List").fields.getByTitle("My Field").setShowInNewForm(true);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/files/index.html b/v2/sp/files/index.html new file mode 100644 index 000000000..72572674f --- /dev/null +++ b/v2/sp/files/index.html @@ -0,0 +1,2949 @@ + + + + + + + + + + + + + + + + + + Files - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/files

    +

    One of the more challenging tasks on the client side is working with SharePoint files, especially if they are large files. We have added some methods to the library to help and their use is outlined below.

    +

    Reading Files

    +

    Reading files from the client using REST is covered in the below examples. The important thing to remember is choosing which format you want the file in so you can appropriately process it. You can retrieve a file as Blob, Buffer, JSON, or Text. If you have a special requirement you could also write your own parser.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +
    +const blob: Blob = await sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBlob();
    +
    +const buffer: ArrayBuffer = await sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.avi").getBuffer();
    +
    +const json: any = await sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.json").getJSON();
    +
    +const text: string = await sp.web.getFileByServerRelativeUrl("/sites/dev/documents/file.txt").getText();
    +
    +// all of these also work from a file object no matter how you access it
    +const text2: string = await sp.web.getFolderByServerRelativeUrl("/sites/dev/documents").files.getByName("file.txt").getText();
    +
    +

    getFileByUrl

    +

    Added in 2.0.4

    +

    This method supports opening files from sharing links or absolute urls. The file must reside in the site from which you are trying to open the file.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files/web";
    +
    +const url = "{absolute file url OR sharing url}";
    +
    +// file is an IFile and supports all the file operations
    +const file = sp.web.getFileByUrl(url);
    +
    +// for example
    +const fileContent = await file.getText();
    +
    +

    Adding Files

    +

    Likewise you can add files using one of two methods, add or addChunked. AddChunked is appropriate for larger files, generally larger than 10 MB but this may differ based on your bandwidth/latency so you can adjust the code to use the chunked method. The below example shows getting the file object from an input and uploading it to SharePoint, choosing the upload method based on file size.

    +
    declare var require: (s: string) => any;
    +
    +import { ConsoleListener, Logger, LogLevel } from "@pnp/logging";
    +import { sp } from "@pnp/sp";
    +import { Web } from "@pnp/sp/webs";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +import { auth } from "./auth";
    +let $ = require("jquery"); // <-- used here for illustration
    +
    +let siteUrl = "https://mytenant.sharepoint.com/sites/dev";
    +
    +// comment this out for non-node execution
    +// auth(siteUrl);
    +
    +Logger.subscribe(new ConsoleListener());
    +Logger.activeLogLevel = LogLevel.Verbose;
    +
    +let web = Web(siteUrl);
    +
    +$(() => {
    +    $("#testingdiv").append("<button id='thebuttontodoit'>Do It</button>");
    +
    +    $("#thebuttontodoit").on('click', async (e) => {
    +
    +        e.preventDefault();
    +
    +        let input = <HTMLInputElement>document.getElementById("thefileinput");
    +        let file = input.files[0];
    +
    +        // you can adjust this number to control what size files are uploaded in chunks
    +        if (file.size <= 10485760) {
    +
    +            // small upload
    +            await web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add(file.name, file, true);
    +            Logger.write("done");
    +        } else {
    +
    +            // large upload
    +            await web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.addChunked(file.name, file, data => {
    +
    +                Logger.log({ data: data, level: LogLevel.Verbose, message: "progress" });
    +
    +            }, true);
    +            Logger.write("done!")
    +        }
    +    });
    +});
    +
    +

    Adding a file using Nodejs Streams

    +

    If you are working in nodejs you can also add a file using a stream. This example makes a copy of a file using streams.

    +
    // triggers auto-application of extensions, in this case to add getStream
    +import "@pnp/nodejs";
    +
    +// get a stream of an existing file
    +const sr = await sp.web.getFileByServerRelativePath("/sites/dev/shared documents/old.md").getStream();
    +
    +// now add the stream as a new file, remember to set the content-length header
    +const fr = await sp.web.lists.getByTitle("Documents").rootFolder.files.configure({
    +    headers: {
    +        "content-length": `${sr.knownLength}`,
    +    },
    +}).add("new.md", sr.body);
    +
    +

    Setting Associated Item Values

    +

    You can also update the file properties of a newly uploaded file using code similar to the below snippet:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +
    +const file = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.add("file.name", "file", true);
    +const item = await file.file.getItem();
    +await item.update({
    +  Title: "A Title",
    +  OtherField: "My Other Value"
    +});
    +
    +

    AddUsingPath

    +

    If you need to support the percent or pound characters you can use the addUsingPath method of IFiles

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +
    +const file = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared%20Documents/test/").files.addUsingPath("file%#%.name", "content");
    +
    +

    Update File Content

    +

    You can of course use similar methods to update existing files as shown below. This overwrites the existing content in the file.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.txt").setContent("New string content for the file.");
    +
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/documents/test.mp4").setContentChunked(file);
    +
    +

    Check in, Check out, and Approve & Deny

    +

    The library provides helper methods for checking in, checking out, and approving files. Examples of these methods are shown below.

    +

    Check In

    +

    Check in takes two optional arguments, comment and check in type.

    +
    import { sp } from "@pnp/sp";
    +import { CheckinType } from "@pnp/sp/files";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +// default options with empty comment and CheckinType.Major
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin();
    +console.log("File checked in!");
    +
    +// supply a comment (< 1024 chars) and using default check in type CheckinType.Major
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment");
    +console.log("File checked in!");
    +
    +// Supply both comment and check in type
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkin("A comment", CheckinType.Overwrite);
    +console.log("File checked in!");
    +
    +

    Check Out

    +

    Check out takes no arguments.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").checkout();
    +console.log("File checked out!");
    +
    +

    Approve and Deny

    +

    You can also approve or deny files in libraries that use approval. Approve takes a single required argument of comment, the comment is optional for deny.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").approve("Approval Comment");
    +console.log("File approved!");
    +
    +// deny with no comment
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny();
    +console.log("File denied!");
    +
    +// deny with a supplied comment.
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").deny("Deny comment");
    +console.log("File denied!");
    +
    +

    Publish and Unpublish

    +

    You can both publish and unpublish a file using the library. Both methods take an optional comment argument.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +// publish with no comment
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish();
    +console.log("File published!");
    +
    +// publish with a supplied comment.
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").publish("Publish comment");
    +console.log("File published!");
    +
    +// unpublish with no comment
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish();
    +console.log("File unpublished!");
    +
    +// unpublish with a supplied comment.
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/shared documents/file.txt").unpublish("Unpublish comment");
    +console.log("File unpublished!");
    +
    +

    Advanced Upload Options

    +

    Both the addChunked and setContentChunked methods support options beyond just supplying the file content.

    +

    progress function

    +

    A method that is called each time a chunk is uploaded and provides enough information to report progress or update a progress bar easily. The method has the signature:

    +

    (data: ChunkedFileUploadProgressData) => void

    +

    The data interface is:

    +
    export interface ChunkedFileUploadProgressData {
    +    stage: "starting" | "continue" | "finishing";
    +    blockNumber: number;
    +    totalBlocks: number;
    +    chunkSize: number;
    +    currentPointer: number;
    +    fileSize: number;
    +}
    +
    +

    chunkSize

    +

    This property controls the size of the individual chunks and is defaulted to 10485760 bytes (10 MB). You can adjust this based on your bandwidth needs - especially if writing code for mobile uploads or you are seeing frequent timeouts.

    +

    getItem

    +

    This method allows you to get the item associated with this file. You can optionally specify one or more select fields. The result will be merged with a new Item instance so you will have both the returned property values and chaining ability in a single object.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/security";
    +
    +const item = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem();
    +console.log(item);
    +
    +const item2 = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem("Title", "Modified");
    +console.log(item2);
    +
    +// you can also chain directly off this item instance
    +const perms = await item.getCurrentUserEffectivePermissions();
    +console.log(perms);
    +
    +

    You can also supply a generic typing parameter and the resulting type will be a union type of Item and the generic type parameter. This allows you to have proper intellisense and type checking.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/items";
    +import "@pnp/sp/security";
    +
    +// also supports typing the objects so your type will be a union type
    +const item = await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.txt").getItem<{ Id: number, Title: string }>("Id", "Title");
    +
    +// You get intellisense and proper typing of the returned object
    +console.log(`Id: ${item.Id} -- ${item.Title}`);
    +
    +// You can also chain directly off this item instance
    +const perms = await item.getCurrentUserEffectivePermissions();
    +console.log(perms);
    +
    +

    move

    +

    It's possible to move a file to a new destination within a site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +// destination is a server-relative url of a new file
    +const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;
    +
    +await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").moveTo(destinationUrl);
    +
    +

    copy

    +

    It's possible to copy a file to a new destination within a site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +// destination is a server-relative url of a new file
    +const destinationUrl = `/sites/dev/SiteAssets/new-file.docx`;
    +
    +await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyTo(destinationUrl, false);
    +
    +

    move by path

    +

    It's possible to move a file to a new destination within the same or a different site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +// destination is a server-relative url of a new file
    +const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
    +
    +await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").moveByPath(destinationUrl, false, true);
    +
    +

    copy by path

    +

    It's possible to copy a file to a new destination within the same or a different site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +// destination is a server-relative url of a new file
    +const destinationUrl = `/sites/dev2/SiteAssets/new-file.docx`;
    +
    +await sp.web.getFileByServerRelativePath("/sites/dev/Shared Documents/test.docx").copyByPath(destinationUrl, false, true);
    +
    +

    getFileById

    +

    You can get a file by Id from a web.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +import { IFile } from "@pnp/sp/files";
    +
    +const file: IFile = sp.web.getFileById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
    +
    +

    delete

    +

    Deletes a file

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +await sp.web.rootFolder.files.getByName("name.txt").delete();
    +
    +

    delete with params

    +

    Added in 2.0.9

    +

    Deletes a file with options

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +await sp.web.rootFolder.files.getByName("name.txt").deleteWithParams({
    +    BypassSharedLock: true,
    +});
    +
    +

    exists

    +

    Added in 2.0.9

    +

    Checks to see if a file exists

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/files";
    +
    +const exists = await sp.web.rootFolder.files.getByName("name.txt").exists();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/folders/index.html b/v2/sp/folders/index.html new file mode 100644 index 000000000..35ed45650 --- /dev/null +++ b/v2/sp/folders/index.html @@ -0,0 +1,2968 @@ + + + + + + + + + + + + + + + + + + Folders - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/folders

    +

    Folders serve as a container for your files and list items.

    +

    IFolders

    +

    Invokable Banner Selective Imports Banner

    +

    Represents a collection of folders. SharePoint webs, lists, and list items have a collection of folders under their properties.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import { IFolders, Folders } from "@pnp/sp/folders";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders/web";
    Selective 4import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders/list";
    Selective 5import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders/list";
    import "@pnp/sp/folders/item";
    Preset: Allimport { sp, IFolders, Folders } from "@pnp/sp/presets/all";
    +

    Get folders collection for various SharePoint objects

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/items";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/lists";
    +
    +// gets web's folders
    +const webFolders = await sp.web.folders();
    +
    +// gets list's folders
    +const listFolders = await sp.web.lists.getByTitle("My List").rootFolder.folders();
    +
    +// gets item's folders
    +const itemFolders = await sp.web.lists.getByTitle("My List").items.getById(1).folder.folders();
    +
    +

    add

    +

    Adds a new folder to collection of folders

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +// creates a new folder for web with specified url
    +const folderAddResult = await sp.web.folders.add("folder url");
    +
    +

    getByName

    +

    Gets a folder instance from a collection by folder's name

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const folder = await sp.web.folders.getByName("folder name")();
    +
    +

    IFolder

    +

    Represents an instance of a SharePoint folder.

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import { IFolders, Folders } from "@pnp/sp/folders";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders/web";
    Selective 4import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders/list";
    Selective 5import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/folders/list";
    import "@pnp/sp/folders/item";
    Preset: Allimport { sp, IFolders, Folders } from "@pnp/sp/presets/all";
    +

    Get a folder object associated with different SharePoint artifacts (web, list, list item)

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +// web's folder
    +const rootFolder = await sp.web.rootFolder();
    +
    +// list's folder
    +const listRootFolder = await sp.web.lists.getByTitle("234").rootFolder();
    +
    +// item's folder
    +const itemFolder = await sp.web.lists.getByTitle("234").items.getById(1).folder();
    +
    +

    getItem

    +

    Gets list item associated with a folder

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const folderItem = await sp.web.rootFolder.folders.getByName("SiteAssets").folders.getByName("My Folder").getItem();
    +
    +

    move

    +

    It's possible to move a folder to a new destination within a site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +// destination is a server-relative url of a new folder
    +const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
    +
    +await sp.web.rootFolder.folders.getByName("SiteAssets").folders.getByName("My Folder").moveTo(destinationUrl);
    +
    +

    copy

    +

    It's possible to copy a folder to a new destination within a site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +// destination is a server-relative url of a new folder
    +const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
    +
    +await sp.web.rootFolder.folders.getByName("SiteAssets").folders.getByName("My Folder").copyTo(destinationUrl);
    +
    +

    move by path

    +

    It's possible to move a folder to a new destination within the same or a different site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +// destination is a server-relative url of a new folder
    +const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
    +
    +await sp.web.rootFolder.folders.getByName("SiteAssets").folders.getByName("My Folder").moveByPath(destinationUrl, true);
    +
    +

    copy by path

    +

    It's possible to copy a folder to a new destination within the same or a different site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +// destination is a server-relative url of a new folder
    +const destinationUrl = `/sites/my-site/SiteAssets/new-folder`;
    +
    +await sp.web.rootFolder.folders.getByName("SiteAssets").folders.getByName("My Folder").copyByPath(destinationUrl, true);
    +
    +

    delete

    +

    Deletes a folder

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +await sp.web.rootFolder.folders.getByName("My Folder").delete();
    +
    +

    delete with params

    +

    Added in 2.0.9

    +

    Deletes a folder with options

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +await sp.web.rootFolder.folders.getByName("My Folder").deleteWithParams({
    +                BypassSharedLock: true,
    +                DeleteIfEmpty: true,
    +            });
    +
    +

    recycle

    +

    Recycles a folder

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +await sp.web.rootFolder.folders.getByName("My Folder").recycle();
    +
    +

    serverRelativeUrl

    +

    Gets folder's server relative url

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const relUrl = await sp.web.rootFolder.folders.getByName("SiteAssets").serverRelativeUrl();
    +
    +

    update

    +

    Updates folder's properties

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").update({
    +        "Name": "New name",
    +    });
    +
    +

    contentTypeOrder

    +

    Gets content type order of a folder

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const order = await sp.web.getFolderByServerRelativePath("Shared Documents").contentTypeOrder();
    +
    +

    folders

    +

    Gets all child folders associated with the current folder

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const folders = await sp.web.rootFolder.folders();
    +
    +

    files

    +

    Gets all files inside a folder

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/files/folder";
    +
    +const files = await sp.web.getFolderByServerRelativePath("Shared Documents").files();
    +
    +

    listItemAllFields

    +

    Gets this folder's list item field values

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const itemFields = await sp.web.getFolderByServerRelativePath("Shared Documents/My Folder").listItemAllFields();
    +
    +

    parentFolder

    +

    Gets the parent folder, if available

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const parentFolder = await sp.web.getFolderByServerRelativePath("Shared Documents/My Folder").parentFolder();
    +
    +

    properties

    +

    Gets this folder's properties

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const properties = await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").properties();
    +
    +

    uniqueContentTypeOrder

    +

    Gets a value that specifies the content type order.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const contentTypeOrder = await sp.web.getFolderByServerRelativePath("Shared Documents/Folder2").uniqueContentTypeOrder();
    +
    +

    Rename a folder

    +

    You can rename a folder by updating FileLeafRef property:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const folder = sp.web.getFolderByServerRelativePath("Shared Documents/My Folder");
    +
    +const item = await folder.getItem();
    +const result = await item.update({ FileLeafRef: "Folder2" });
    +
    +

    Create a folder with custom content type

    +

    Below code creates a new folder under Document library and assigns custom folder content type to a newly created folder. Additionally it sets a field of a custom folder content type.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/items";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/lists";
    +
    +const newFolderResult = await sp.web.rootFolder.folders.getByName("Shared Documents").folders.add("My New Folder");
    +const item = await newFolderResult.folder.listItemAllFields();
    +
    +await sp.web.lists.getByTitle("Documents").items.getById(item.ID).update({
    +    ContentTypeId: "0x0120001E76ED75A3E3F3408811F0BF56C4CDDD",
    +    MyFolderField: "field value",
    +    Title: "My New Folder",
    +});
    +
    +

    addSubFolderUsingPath

    +

    Added in 2.0.9

    +

    You can use the addSubFolderUsingPath method to add a folder with some special chars supported

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +import { IFolder } from "@pnp/sp/folders";
    +
    +// add a folder to site assets
    +const folder: IFolder = await web.rootFolder.folders.getByName("SiteAssets").addSubFolderUsingPath("folder name");
    +
    +

    getFolderById

    +

    You can get a folder by Id from a web.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +import { IFolder } from "@pnp/sp/folders";
    +
    +const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
    +
    +

    getParentInfos

    +

    Added in 2.0.12

    +

    Gets information about folder, including details about the parent list, parent list root folder, and parent web.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/folders";
    +
    +const folder: IFolder = sp.web.getFolderById("2b281c7b-ece9-4b76-82f9-f5cf5e152ba0");
    +await folder.getParentInfos();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/forms/index.html b/v2/sp/forms/index.html new file mode 100644 index 000000000..c72177b9b --- /dev/null +++ b/v2/sp/forms/index.html @@ -0,0 +1,2289 @@ + + + + + + + + + + + + + + + + + + Forms - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/forms

    +

    Forms in SharePoint are the Display, New, and Edit forms associated with a list.

    +

    IFields

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Webs, IWebs } from "@pnp/sp/webs";
    import "@pnp/sp/forms";
    import "@pnp/sp/lists";
    +

    Get Form by Id

    +

    Gets a form from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/forms";
    +import "@pnp/sp/lists";
    +
    +// get the field by Id for web
    +const form = sp.web.lists.getByTitle("Documents").forms.getById("{c4486774-f1e2-4804-96f3-91edf3e22a19}")();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/hubsites/index.html b/v2/sp/hubsites/index.html new file mode 100644 index 000000000..aa86b669f --- /dev/null +++ b/v2/sp/hubsites/index.html @@ -0,0 +1,2485 @@ + + + + + + + + + + + + + + + + + + Hubsites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/hubsites

    +

    This module helps you with working with hub sites in your tenant.

    +

    IHubSites

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { sp } from "@pnp/sp";
    import "@pnp/sp/hubsites";
    Preset: Allimport { sp, HubSites, IHubSites } from "@pnp/sp/presets/all";
    +

    Get a Listing of All Hub sites

    +
    import { sp } from "@pnp/sp";
    +import { IHubSiteInfo } from  "@pnp/sp/hubsites";
    +import "@pnp/sp/hubsites";
    +
    +// invoke the hub sites object
    +const hubsites: IHubSiteInfo[] = await sp.hubSites();
    +
    +// you can also use select to only return certain fields:
    +const hubsites2: IHubSiteInfo[] = await sp.hubSites.select("ID", "Title", "RelatedHubSiteIds")();
    +
    +

    Get Hub site by Id

    +

    Using the getById method on the hubsites module to get a hub site by site Id (guid).

    +
    import { sp } from "@pnp/sp";
    +import { IHubSiteInfo } from  "@pnp/sp/hubsites";
    +import "@pnp/sp/hubsites";
    +
    +const hubsite: IHubSiteInfo = await sp.hubSites.getById("3504348e-b2be-49fb-a2a9-2d748db64beb")();
    +
    +// log hub site title to console
    +console.log(hubsite.Title);
    +
    +

    Get ISite instance

    +

    We provide a helper method to load the ISite instance from the HubSite

    +
    import { sp } from "@pnp/sp";
    +import { ISite } from  "@pnp/sp/sites";
    +import "@pnp/sp/hubsites";
    +
    +const site: ISite = await sp.hubSites.getById("3504348e-b2be-49fb-a2a9-2d748db64beb").getSite();
    +
    +const siteData = await site();
    +
    +console.log(siteData.Title);
    +
    +

    Get Hub site data for a web

    +
    import { sp } from "@pnp/sp";
    +import { IHubSiteWebData } from  "@pnp/sp/hubsites";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/hubsites/web";
    +
    +const webData: Partial<IHubSiteWebData> = await sp.web.hubSiteData();
    +
    +// you can also force a refresh of the hub site data
    +const webData2: Partial<IHubSiteWebData> = await sp.web.hubSiteData(true);
    +
    +

    syncHubSiteTheme

    +

    Allows you to apply theme updates from the parent hub site collection.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/hubsites/web";
    +
    +await sp.web.syncHubSiteTheme();
    +
    +

    Hub site Site Methods

    +

    You manage hub sites at the Site level.

    +

    joinHubSite

    +

    Id of the hub site collection you want to join. If you want to disassociate the site collection from hub site, then pass the siteId as 00000000-0000-0000-0000-000000000000

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +import "@pnp/sp/hubsites/site";
    +
    +// join a site to a hub site
    +await sp.site.joinHubSite("{parent hub site id}");
    +
    +// remove a site from a hub site
    +await sp.site.joinHubSite("00000000-0000-0000-0000-000000000000");
    +
    +

    registerHubSite

    +

    Registers the current site collection as hub site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +import "@pnp/sp/hubsites/site";
    +
    +// register current site as a hub site
    +await sp.site.registerHubSite();
    +
    +

    unRegisterHubSite

    +

    Un-registers the current site collection as hub site collection.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +import "@pnp/sp/hubsites/site";
    +
    +// make a site no longer a hub
    +await sp.site.unRegisterHubSite();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/index.html b/v2/sp/index.html new file mode 100644 index 000000000..bc732bbb2 --- /dev/null +++ b/v2/sp/index.html @@ -0,0 +1,2328 @@ + + + + + + + + + + + + + + + + + + sp - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp

    +

    npm version

    +

    This package contains the fluent api used to call the SharePoint rest services.

    +

    Getting Started

    +

    Install the library and required dependencies

    +

    npm install @pnp/sp --save

    +

    Import the library into your application and access the root sp object

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +(function main() {
    +
    +    // here we will load the current web's title
    +    const w = await sp.web.select("Title")();
    +    console.log(`Web Title: ${w.Title}`);
    +)()
    +
    +

    Getting Started: SharePoint Framework

    +

    Install the library and required dependencies

    +

    npm install @pnp/sp --save

    +

    Import the library into your application, update OnInit, and access the root sp object in render

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// ...
    +
    +public onInit(): Promise<void> {
    +
    +  return super.onInit().then(_ => {
    +
    +    // other init code may be present
    +
    +    sp.setup({
    +      spfxContext: this.context
    +    });
    +  });
    +}
    +
    +// ...
    +
    +public render(): void {
    +
    +    // A simple loading message
    +    this.domElement.innerHTML = `Loading...`;
    +
    +    const w = await sp.web.select("Title")();
    +    this.domElement.innerHTML = `Web Title: ${w.Title}`;
    +}
    +
    +

    Getting Started: Nodejs

    +

    Install the library and required dependencies

    +

    npm install @pnp/sp @pnp/nodejs --save

    +

    Import the library into your application, setup the node client, make a request

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import { SPFetchClient } from "@pnp/nodejs";
    +
    +// do this once per page load
    +sp.setup({
    +    sp: {
    +        fetchClientFactory: () => {
    +            return new SPFetchClient("{your site url}", "{your client id}", "{your client secret}");
    +        },
    +    },
    +});
    +
    +// now make any calls you need using the configured client
    +
    +const w = await sp.web.select("Title")();
    +console.log(`Web Title: ${w.Title}`);
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/items/index.html b/v2/sp/items/index.html new file mode 100644 index 000000000..002e57ca7 --- /dev/null +++ b/v2/sp/items/index.html @@ -0,0 +1,2974 @@ + + + + + + + + + + + + + + + + + + List Items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/items

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/lists";
    import "@pnp/sp/items";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp } from "@pnp/sp/presets/core";
    +

    GET

    +

    Getting items from a list is one of the basic actions that most applications require. This is made easy through the library and the following examples demonstrate these actions.

    +

    Basic Get

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +// get all the items from a list
    +const items: any[] = await sp.web.lists.getByTitle("My List").items();
    +console.log(items);
    +
    +// get a specific item by id.
    +const item: any = await sp.web.lists.getByTitle("My List").items.getById(1)();
    +console.log(item);
    +
    +// use odata operators for more efficient queries
    +const items2: any[] = await sp.web.lists.getByTitle("My List").items.select("Title", "Description").top(5).orderBy("Modified", true)();
    +console.log(items2);
    +
    +

    Get Paged Items

    +

    Working with paging can be a challenge as it is based on skip tokens and item ids, something that is hard to guess at runtime. To simplify things you can use the getPaged method on the Items class to assist. Note that there isn't a way to move backwards in the collection, this is by design. The pattern you should use to support backwards navigation in the results is to cache the results into a local array and use the standard array operators to get previous pages. Alternatively you can append the results to the UI, but this can have performance impact for large result sets.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +// basic case to get paged items form a list
    +let items = await sp.web.lists.getByTitle("BigList").items.getPaged();
    +
    +// you can also provide a type for the returned values instead of any
    +let items = await sp.web.lists.getByTitle("BigList").items.getPaged<{Title: string}[]>();
    +
    +// the query also works with select to choose certain fields and top to set the page size
    +let items = await sp.web.lists.getByTitle("BigList").items.select("Title", "Description").top(50).getPaged<{Title: string}[]>();
    +
    +// the results object will have two properties and one method:
    +
    +// the results property will be an array of the items returned
    +if (items.results.length > 0) {
    +    console.log("We got results!");
    +
    +    for (let i = 0; i < items.results.length; i++) {
    +        // type checking works here if we specify the return type
    +        console.log(items.results[i].Title);
    +    }
    +}
    +
    +// the hasNext property is used with the getNext method to handle paging
    +// hasNext will be true so long as there are additional results
    +if (items.hasNext) {
    +
    +    // this will carry over the type specified in the original query for the results array
    +    items = await items.getNext();
    +    console.log(items.results.length);
    +}
    +
    +

    getListItemChangesSinceToken

    +

    The GetListItemChangesSinceToken method allows clients to track changes on a list. Changes, including deleted items, are returned along with a token that represents the moment in time when those changes were requested. By including this token when you call GetListItemChangesSinceToken, the server looks for only those changes that have occurred since the token was generated. Sending a GetListItemChangesSinceToken request without including a token returns the list schema, the full list contents and a token.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +
    +// Using RowLimit. Enables paging
    +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({RowLimit: '5'});
    +
    +// Use QueryOptions to make a XML-style query.
    +// Because it's XML we need to escape special characters
    +// Instead of & we use &amp; in the query
    +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({QueryOptions: '<Paging ListItemCollectionPositionNext="Paged=TRUE&amp;p_ID=5" />'});
    +
    +// Get everything. Using null with ChangeToken gets everything
    +let changes = await sp.web.lists.getByTitle("BigList").getListItemChangesSinceToken({ChangeToken: null});
    +
    +
    +

    Get All Items

    +

    Using the items collection's getAll method you can get all of the items in a list regardless of the size of the list. Sample usage is shown below. Only the odata operations top, select, and filter are supported. usingCaching and inBatch are ignored - you will need to handle caching the results on your own. This method will write a warning to the Logger and should not frequently be used. Instead the standard paging operations should be used.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +// basic usage
    +const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.getAll();
    +console.log(allItems.length);
    +
    +// set page size
    +const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.getAll(4000);
    +console.log(allItems.length);
    +
    +// use select and top. top will set page size and override the any value passed to getAll
    +const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.select("Title").top(4000).getAll();
    +console.log(allItems.length);
    +
    +// we can also use filter as a supported odata operation, but this will likely fail on large lists
    +const allItems: any[] = await sp.web.lists.getByTitle("BigList").items.select("Title").filter("Title eq 'Test'").getAll();
    +console.log(allItems.length);
    +
    +

    Retrieving Lookup Fields

    +

    When working with lookup fields you need to use the expand operator along with select to get the related fields from the lookup column. This works for both the items collection and item instances.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +const items = await sp.web.lists.getByTitle("LookupList").items.select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup")();
    +console.log(items);
    +
    +const item = await sp.web.lists.getByTitle("LookupList").items.getById(1).select("Title", "Lookup/Title", "Lookup/ID").expand("Lookup")();
    +console.log(item);
    +
    +

    Filter using Metadata fields

    +

    To filter on a metadata field you must use the getItemsByCAMLQuery method as $filter does not support these fields.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +
    +const r = await sp.web.lists.getByTitle("TaxonomyList").getItemsByCAMLQuery({
    +    ViewXml: `<View><Query><Where><Eq><FieldRef Name="MetaData"/><Value Type="TaxonomyFieldType">Term 2</Value></Eq></Where></Query></View>`,
    +});
    +
    +

    Retrieving PublishingPageImage

    +

    The PublishingPageImage and some other publishing-related fields aren't stored in normal fields, rather in the MetaInfo field. To get these values you need to use the technique shown below, and originally outlined in this thread. Note that a lot of information can be stored in this field so will pull back potentially a significant amount of data, so limit the rows as possible to aid performance.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +import { Web } from "@pnp/sp/webs";
    +try {
    +  const w = Web("https://{publishing site url}");
    +  const r = await w.lists.getByTitle("Pages").items
    +    .select("Title", "FileRef", "FieldValuesAsText/MetaInfo")
    +    .expand("FieldValuesAsText")
    +    ();
    +
    +  // look through the returned items.
    +  for (var i = 0; i < r.length; i++) {
    +
    +    // the title field value
    +    console.log(r[i].Title);
    +
    +    // find the value in the MetaInfo string using regex
    +    const matches = /PublishingPageImage:SW\|(.*?)\r\n/ig.exec(r[i].FieldValuesAsText.MetaInfo);
    +    if (matches !== null && matches.length > 1) {
    +
    +      // this wil be the value of the PublishingPageImage field
    +      console.log(matches[1]);
    +    }
    +  }
    +}
    +catch (e) {
    +  console.error(e);
    +}
    +
    +

    Add Items

    +

    There are several ways to add items to a list. The simplest just uses the add method of the items collection passing in the properties as a plain object.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +import { IItemAddResult } from "@pnp/sp/items";
    +
    +// add an item to the list
    +const iar: IItemAddResult = await sp.web.lists.getByTitle("My List").items.add({
    +  Title: "Title",
    +  Description: "Description"
    +});
    +
    +console.log(iar);
    +
    +

    Content Type

    +

    You can also set the content type id when you create an item as shown in the example below. For more information on content type IDs reference the Microsoft Documentation. While this documentation references SharePoint 2010 the structure of the IDs has not changed.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +await sp.web.lists.getById("4D5A36EA-6E84-4160-8458-65C436DB765C").items.add({
    +    Title: "Test 1",
    +    ContentTypeId: "0x01030058FD86C279252341AB303852303E4DAF"
    +});
    +
    +

    User Fields

    +

    There are two types of user fields, those that allow a single value and those that allow multiple. For both types, you first need to determine the Id field name, which you can do by doing a GET REST request on an existing item. Typically the value will be the user field internal name with "Id" appended. So in our example, we have two fields User1 and User2 so the Id fields are User1Id and User2Id.

    +

    Next, you need to remember there are two types of user fields, those that take a single value and those that allow multiple - these are updated in different ways. For single value user fields you supply just the user's id. For multiple value fields, you need to supply an object with a "results" property and an array. Examples for both are shown below.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +import { getGUID } from "@pnp/core";
    +
    +const i = await sp.web.lists.getByTitle("PeopleFields").items.add({
    +  Title: getGUID(),
    +  User1Id: 9, // allows a single user
    +  User2Id: {
    +    results: [16, 45] // allows multiple users
    +  }
    +});
    +
    +console.log(i);
    +
    +

    If you want to update or add user field values when using validateUpdateListItem you need to use the form shown below. You can specify multiple values in the array.

    +
    import { sp } from "@pnp/sp";
    +
    +const result = await sp.web.lists.getByTitle("UserFieldList").items.getById(1).validateUpdateListItem([{
    +    FieldName: "UserField",
    +    FieldValue: JSON.stringify([{ "Key": "i:0#.f|membership|person@tenant.com" }]),
    +},
    +{
    +    FieldName: "Title",
    +    FieldValue: "Test - Updated",
    +}]);
    +
    +

    Lookup Fields

    +

    What is said for User Fields is, in general, relevant to Lookup Fields:

    +
      +
    • Lookup Field types:
    • +
    • Single-valued lookup
    • +
    • Multiple-valued lookup
    • +
    • Id suffix should be appended to the end of lookups EntityPropertyName in payloads
    • +
    • Numeric Ids for lookups' items should be passed as values
    • +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +import { getGUID } from "@pnp/core";
    +
    +await sp.web.lists.getByTitle("LookupFields").items.add({
    +    Title: getGUID(),
    +    LookupFieldId: 2,       // allows a single lookup value
    +    MultiLookupFieldId: {
    +        results: [ 1, 56 ]  // allows multiple lookup value
    +    }
    +});
    +
    +

    Add Multiple Items

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +let list = sp.web.lists.getByTitle("rapidadd");
    +
    +const entityTypeFullName = await list.getListItemEntityTypeFullName()
    +
    +let batch = sp.web.createBatch();
    +
    +list.items.inBatch(batch).add({ Title: "Batch 6" }, entityTypeFullName).then(b => {
    +  console.log(b);
    +});
    +
    +list.items.inBatch(batch).add({ Title: "Batch 7" }, entityTypeFullName).then(b => {
    +  console.log(b);
    +});
    +
    +await batch.execute();
    +console.log("Done");
    +
    +

    Update

    +

    The update method is very similar to the add method in that it takes a plain object representing the fields to update. The property names are the internal names of the fields. If you aren't sure you can always do a get request for an item in the list and see the field names that come back - you would use these same names to update the item.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +let list = sp.web.lists.getByTitle("MyList");
    +
    +const i = await list.items.getById(1).update({
    +  Title: "My New Title",
    +  Description: "Here is a new description"
    +});
    +
    +console.log(i);
    +
    +

    Getting and updating a collection using filter

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +// you are getting back a collection here
    +const items: any[] = await sp.web.lists.getByTitle("MyList").items.top(1).filter("Title eq 'A Title'")();
    +
    +// see if we got something
    +if (items.length > 0) {
    +  const updatedItem = await sp.web.lists.getByTitle("MyList").items.getById(items[0].Id).update({
    +    Title: "Updated Title",
    +  });
    +
    +  console.log(JSON.stringify(updatedItem));
    +}
    +
    +

    Update Multiple Items

    +

    This approach avoids multiple calls for the same list's entity type name.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +let list = sp.web.lists.getByTitle("rapidupdate");
    +
    +const entityTypeFullName = await list.getListItemEntityTypeFullName()
    +
    +let batch = sp.web.createBatch();
    +
    +// note requirement of "*" eTag param - or use a specific eTag value as needed
    +list.items.getById(1).inBatch(batch).update({ Title: "Batch 6" }, "*", entityTypeFullName).then(b => {
    +  console.log(b);
    +});
    +
    +list.items.getById(2).inBatch(batch).update({ Title: "Batch 7" }, "*", entityTypeFullName).then(b => {
    +  console.log(b);
    +});
    +
    +await batch.execute();
    +console.log("Done")
    +
    +
    +

    Recycle

    +

    To send an item to the recycle bin use recycle.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +let list = sp.web.lists.getByTitle("MyList");
    +
    +const recycleBinIdentifier = await list.items.getById(1).recycle();
    +
    +

    Delete

    +

    Delete is as simple as calling the .delete method. It optionally takes an eTag if you need to manage concurrency.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +let list = sp.web.lists.getByTitle("MyList");
    +
    +await list.items.getById(1).delete();
    +
    +

    Delete With Params

    +

    Added in 2.0.9

    +

    Deletes the item object with options.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +
    +let list = sp.web.lists.getByTitle("MyList");
    +
    +await list.items.getById(1).deleteWithParams({
    +                BypassSharedLock: true,
    +            });
    +
    +
    +

    The deleteWithParams method can only be used by accounts where UserToken.IsSystemAccount is true

    +
    +

    Resolving field names

    +

    It's a very common mistake trying wrong field names in the requests. +Field's EntityPropertyName value should be used.

    +

    The easiest way to get know EntityPropertyName is to use the following snippet:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/items";
    +import "@pnp/sp/fields";
    +
    +const response =
    +  await sp.web.lists
    +    .getByTitle('[Lists_Title]')
    +    .fields
    +    .select('Title, EntityPropertyName')
    +    .filter(`Hidden eq false and Title eq '[Field's_Display_Name]'`)
    +    ();
    +
    +console.log(response.map(field => {
    +  return {
    +    Title: field.Title,
    +    EntityPropertyName: field.EntityPropertyName
    +  };
    +}));
    +
    +

    Lookup fields' names should be ended with additional Id suffix. E.g. for Editor EntityPropertyName EditorId should be used.

    +

    getParentInfos

    +

    Added in 2.0.12

    +

    Gets information about an item, including details about the parent list, parent list root folder, and parent web.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/items";
    +
    +const item: any = await sp.web.lists.getByTitle("My List").items.getById(1)();
    +await item.getParentInfos();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/lists/index.html b/v2/sp/lists/index.html new file mode 100644 index 000000000..a11f5a42b --- /dev/null +++ b/v2/sp/lists/index.html @@ -0,0 +1,3387 @@ + + + + + + + + + + + + + + + + + + Lists - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/lists

    +

    Lists in SharePoint are collections of information built in a structural way using columns and rows. Columns for metadata, and rows representing each entry. Visually, it reminds us a lot of a database table or an Excel spreadsheet.

    +

    ILists

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Webs, IWebs } from "@pnp/sp/webs";
    import { Lists, ILists } from "@pnp/sp/lists";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/lists";
    Preset: Allimport { sp, Lists, ILists } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp, Lists, ILists } from "@pnp/sp/presets/core";
    +

    Get List by Id

    +

    Gets a list from the collection by id (guid). Note that the library will handle a guid formatted with curly braces (i.e. '{03b05ff4-d95d-45ed-841d-3855f77a2483}') as well as without curly braces (i.e. '03b05ff4-d95d-45ed-841d-3855f77a2483'). The Id parameter is also case insensitive.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +
    +// get the list by Id
    +const list = sp.web.lists.getById("03b05ff4-d95d-45ed-841d-3855f77a2483");
    +
    +// we can use this 'list' variable to execute more queries on the list:
    +const r = await list.select("Title")();
    +
    +// show the response from the server
    +console.log(r.Title);
    +
    +

    Get List by Title

    +

    You can also get a list from the collection by title.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +
    +// get the default document library 'Documents'
    +const list = sp.web.lists.getByTitle("Documents");
    +
    +// we can use this 'list' variable to run more queries on the list:
    +const r = await list.select("Id")();
    +
    +// log the list Id to console
    +console.log(r.Id);
    +
    +

    Add List

    +

    You can add a list to the web's list collection using the .add-method. To invoke this method in its most simple form, you can provide only a title as a parameter. This will result in a standard out of the box list with all default settings, and the title you provide.

    +
    // create a new list, passing only the title
    +const listAddResult = await sp.web.lists.add("My new list");
    +
    +// we can work with the list created using the IListAddResult.list property:
    +const r = await listAddResult.list.select("Title")();
    +
    +// log newly created list title to console
    +console.log(r.Title);
    +});
    +
    +

    You can also provide other (optional) parameters like description, template and enableContentTypes. If that is not enough for you, you can use the parameter named 'additionalSettings' which is just a TypedHash, meaning you can sent whatever properties you'd like in the body (provided that the property is supported by the SharePoint API). You can find a listing of list template codes in the official docs.

    +
    // this will create a list with template 101 (Document library), content types enabled and show it on the quick launch (using additionalSettings)
    +const listAddResult = await sp.web.lists.add("My Doc Library", "This is a description of doc lib.", 101, true, { OnQuickLaunch: true });
    +
    +// get the Id of the newly added document library
    +const r = await listAddResult.list.select("Id")();
    +
    +// log id to console
    +console.log(r.Id);
    +
    +

    Ensure that a List exists (by title)

    +

    Ensures that the specified list exists in the collection (note: this method not supported for batching). Just like with the add-method (see examples above) you can provide only the title, or any or all of the optional parameters desc, template, enableContentTypes and additionalSettings.

    +
    // ensure that a list exists. If it doesn't it will be created with the provided title (the rest of the settings will be default):
    +const listEnsureResult = await sp.web.lists.ensure("My List");
    +
    +// check if the list was created, or if it already existed:
    +if (listEnsureResult.created) {
    +    console.log("My List was created!");
    +} else {
    +    console.log("My List already existed!");
    +}
    +
    +// work on the created/updated list
    +const r = await listEnsureResult.list.select("Id")();
    +
    +// log the Id
    +console.log(r.Id);
    +
    +

    If the list already exists, the other settings you provide will be used to update the existing list.

    +
    // add a new list to the lists collection of the web
    +sp.web.lists.add("My List 2").then(async () => {
    +
    +// then call ensure on the created list with an updated description
    +const listEnsureResult = await sp.web.lists.ensure("My List 2", "Updated description");
    +
    +// get the updated description
    +const r = await listEnsureResult.list.select("Description")();
    +
    +// log the updated description
    +console.log(r.Description);
    +});
    +
    +

    Ensure Site Assets Library exist

    +

    Gets a list that is the default asset location for images or other files, which the users upload to their wiki pages.

    +
    // get Site Assets library
    +const siteAssetsList = await sp.web.lists.ensureSiteAssetsLibrary();
    +
    +// get the Title
    +const r = await siteAssetsList.select("Title")();
    +
    +// log Title
    +console.log(r.Title);
    +
    +

    Ensure Site Pages Library exist

    +

    Gets a list that is the default location for wiki pages.

    +
    // get Site Pages library
    +const siteAssetsList = await sp.web.lists.ensureSitePagesLibrary();
    +
    +// get the Title
    +const r = await siteAssetsList.select("Title")();
    +
    +// log Title
    +console.log(r.Title);
    +
    +

    IList

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { List, IList } from "@pnp/sp/lists";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/lists";
    Preset: Allimport { sp, List, IList } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp, List, IList } from "@pnp/sp/presets/core";
    +

    Update a list

    +

    Update an existing list with the provided properties. You can also provide an eTag value that will be used in the IF-Match header (default is "*")

    +
    import { IListUpdateResult } from "@pnp/sp/lists";
    +
    +// create a TypedHash object with the properties to update
    +const updateProperties = {
    +    Description: "This list title and description has been updated using PnPjs.",
    +    Title: "Updated title",
    +};
    +
    +// update the list with the properties above
    +list.update(updateProperties).then(async (l: IListUpdateResult) => {
    +
    +    // get the updated title and description
    +    const r = await l.list.select("Title", "Description")();
    +
    +    // log the updated properties to the console
    +    console.log(r.Title);
    +    console.log(r.Description);
    +});
    +
    +

    Get changes on a list

    +

    From the change log, you can get a collection of changes that have occurred within the list based on the specified query.

    +
    import { sp, IChangeQuery } from "@pnp/sp";
    +
    +// build the changeQuery object, here we look att changes regarding Add, DeleteObject and Restore
    +const changeQuery: IChangeQuery = {
    +    Add: true,
    +    ChangeTokenEnd: null,
    +    ChangeTokenStart: null,
    +    DeleteObject: true,
    +    Rename: true,
    +    Restore: true,
    +};
    +
    +// get list changes
    +const r = await list.getChanges(changeQuery);
    +
    +// log changes to console
    +console.log(r);
    +
    +

    Get list items using a CAML Query

    +

    You can get items from SharePoint using a CAML Query.

    +
    import { ICamlQuery } from "@pnp/sp/lists";
    +
    +// build the caml query object (in this example, we include Title field and limit rows to 5)
    +const caml: ICamlQuery = {
    +    ViewXml: "<View><ViewFields><FieldRef Name='Title' /></ViewFields><RowLimit>5</RowLimit></View>",
    +};
    +
    +// get list items
    +const r = await list.getItemsByCAMLQuery(caml);
    +
    +// log resulting array to console
    +console.log(r);
    +
    +

    If you need to get and expand a lookup field, there is a spread array parameter on the getItemsByCAMLQuery. This means that you can provide multiple properties to this method depending on how many lookup fields you are working with on your list. Below is a minimal example showing how to expand one field (RoleAssignment)

    +
    import { ICamlQuery } from "@pnp/sp/lists";
    +
    +// build the caml query object (in this example, we include Title field and limit rows to 5)
    +const caml: ICamlQuery = {
    +    ViewXml: "<View><ViewFields><FieldRef Name='Title' /><FieldRef Name='RoleAssignments' /></ViewFields><RowLimit>5</RowLimit></View>",
    +};
    +
    +// get list items
    +const r = await list.getItemsByCAMLQuery(caml, "RoleAssignments");
    +
    +// log resulting item array to console
    +console.log(r);
    +
    +

    Get list items changes using a Token

    +
    import {  IChangeLogItemQuery } from "@pnp/sp/lists";
    +
    +// build the caml query object (in this example, we include Title field and limit rows to 5)
    +const changeLogItemQuery: IChangeLogItemQuery = {
    +    Contains: `<Contains><FieldRef Name="Title"/><Value Type="Text">Item16</Value></Contains>`,
    +    QueryOptions: `<QueryOptions>
    +    <IncludeMandatoryColumns>FALSE</IncludeMandatoryColumns>
    +    <DateInUtc>False</DateInUtc>
    +    <IncludePermissions>TRUE</IncludePermissions>
    +    <IncludeAttachmentUrls>FALSE</IncludeAttachmentUrls>
    +    <Folder>My List</Folder></QueryOptions>`,
    +};
    +
    +// get list items
    +const r = await list.getListItemChangesSinceToken(changeLogItemQuery);
    +
    +// log resulting XML to console
    +console.log(r);
    +
    +

    Recycle a list

    +

    Removes the list from the web's list collection and puts it in the recycle bin.

    +
    await list.recycle();
    +
    +

    Render list data

    +
    import { IRenderListData } from "@pnp/sp/lists";
    +
    +// render list data, top 5 items
    +const r: IRenderListData = await list.renderListData("<View><RowLimit>5</RowLimit></View>");
    +
    +// log array of items in response
    +console.log(r.Row);
    +
    +

    Render list data as stream

    +
    import { IRenderListDataParameters } from "@pnp/sp/lists";
    +
    +// setup parameters object
    +const renderListDataParams: IRenderListDataParameters = {
    +    ViewXml: "<View><RowLimit>5</RowLimit></View>",
    +};
    +
    +// render list data as stream
    +const r = await list.renderListDataAsStream(renderListDataParams);
    +
    +// log array of items in response
    +console.log(r.Row);
    +
    +

    Reserve list item Id for idempotent list item creation

    +
    const listItemId = await list.reserveListItemId();
    +
    +// log id to console
    +console.log(listItemId);
    +
    +

    Get list item entity type name

    +
    const entityTypeFullName = await list.getListItemEntityTypeFullName();
    +
    +// log entity type name
    +console.log(entityTypeFullName);
    +
    +

    Add a list item using path (folder), validation and set field values

    +
    const list = await sp.webs.lists.getByTitle("MyList").select("Title", "ParentWebUrl")();
    +const formValues: IListItemFormUpdateValue[] = [
    +                {
    +                    FieldName: "Title",
    +                    FieldValue: title,
    +                },
    +            ];
    +
    +list.addValidateUpdateItemUsingPath(formValues,`${list.ParentWebUrl}/Lists/${list.Title}/MyFolder`)
    +
    +
    +

    content-types imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/content-types";
    Selective 2import "@pnp/sp/content-types/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    contentTypes

    +

    Get all content types for a list

    +
    const list = sp.web.lists.getByTitle("Documents");
    +const r = await list.contentTypes();
    +
    +

    fields imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/fields";
    Selective 2import "@pnp/sp/fields/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    fields

    +

    Get all the fields for a list

    +
    const list = sp.web.lists.getByTitle("Documents");
    +const r = await list.fields();
    +
    +

    Add a field to the site, then add the site field to a list

    +
    const fld = await sp.site.rootWeb.fields.addText("MyField");
    +await sp.web.lists.getByTitle("MyList").fields.createFieldAsXml(fld.data.SchemaXml);
    +
    +

    folders imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/folders";
    Selective 2import "@pnp/sp/folders/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    folders

    +

    Get the root folder of a list.

    +
    const list = sp.web.lists.getByTitle("Documents");
    +const r = await list.rootFolder();
    +
    +

    forms imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/forms";
    Selective 2import "@pnp/sp/forms/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    forms

    +
    const r = await list.forms();
    +
    +

    items imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/items";
    Selective 2import "@pnp/sp/items/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    items

    +

    Get a collection of list items.

    +
    const r = await list.items();
    +
    +

    views imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/views";
    Selective 2import "@pnp/sp/views/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    views

    +

    Get the default view of the list

    +
    const list = sp.web.lists.getByTitle("Documents");
    +const views = await list.views();
    +const defaultView = await list.defaultView();
    +
    +

    Get a list view by Id

    +
    const view = await list.getView(defaultView.Id).select("Title")();
    +
    +

    security imports

    +

    To work with list security, you can import the list methods as follows:

    +
    import "@pnp/sp/security/list";
    +
    +

    For more information on how to call security methods for lists, please refer to the @pnp/sp/security documentation.

    +

    subscriptions imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/subscriptions";
    Selective 2import "@pnp/sp/subscriptions/list";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    subscriptions

    +

    Get all subscriptions on the list

    +
    const list = sp.web.lists.getByTitle("Documents");
    +const subscriptions = await list.subscriptions();
    +
    +

    user-custom-actions imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/user-custom-actions";
    Selective 2import "@pnp/sp/user-custom-actions/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    userCustomActions

    +

    Get a collection of the list's user custom actions.

    +
    const list = sp.web.lists.getByTitle("Documents");
    +const r = await list.userCustomActions();
    +
    +

    getParentInfos

    +

    Added in 2.0.12

    +

    Gets information about an list, including details about the parent list root folder, and parent web.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/items";
    +
    +const list = sp.web.lists.getByTitle("Documents");
    +await list.getParentInfos();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/navigation/index.html b/v2/sp/navigation/index.html new file mode 100644 index 000000000..2391520f9 --- /dev/null +++ b/v2/sp/navigation/index.html @@ -0,0 +1,2535 @@ + + + + + + + + + + + + + + + + + + Navigation - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp - navigation

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/navigation";
    +

    getMenuState

    +

    The MenuState service operation returns a Menu-State (dump) of a SiteMapProvider on a site. It will return an exception if the SiteMapProvider cannot be found on the site, the SiteMapProvider does not implement the IEditableSiteMapProvider interface or the SiteMapNode key cannot be found within the provider hierarchy.

    +

    The IEditableSiteMapProvider also supports Custom Properties which is an optional feature. What will be return in the custom properties is up to the IEditableSiteMapProvider implementation and can differ for for each SiteMapProvider implementation. The custom properties can be requested by providing a comma separated string of property names like: property1,property2,property3\,containingcomma

    +

    NOTE: the , separator can be escaped using the \ as escape character as done in the example above. The string above would split like:

    +
      +
    • property1
    • +
    • property2
    • +
    • property3,containingcomma
    • +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/navigation";
    +
    +// Will return a menu state of the default SiteMapProvider 'SPSiteMapProvider' where the dump starts a the RootNode (within the site) with a depth of 10 levels.
    +const state = await sp.navigation.getMenuState();
    +
    +// Will return the menu state of the 'SPSiteMapProvider', starting with the node with the key '1002' with a depth of 5
    +const state2 = await sp.navigation.getMenuState("1002", 5);
    +
    +// Will return the menu state of the 'CurrentNavSiteMapProviderNoEncode' from the root node of the provider with a depth of 5
    +const state3 = await sp.navigation.getMenuState(null, 5, "CurrentNavSiteMapProviderNoEncode");
    +
    +

    getMenuNodeKey

    +

    Tries to get a SiteMapNode.Key for a given URL within a site collection. If the SiteMapNode cannot be found an Exception is returned. The method is using SiteMapProvider.FindSiteMapNodeFromKey(string rawUrl) to lookup the SiteMapNode. Depending on the actual implementation of FindSiteMapNodeFromKey the matching can differ for different SiteMapProviders.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/navigation";
    +
    +const key = await sp.navigation.getMenuNodeKey("/sites/dev/Lists/SPPnPJSExampleList/AllItems.aspx");
    +
    +

    Web Navigation

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/navigation";
    +

    The navigation object contains two properties "quicklaunch" and "topnavigationbar". Both have the same set of methods so our examples below show use of only quicklaunch but apply equally to topnavigationbar.

    +

    Get navigation

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +const top = await sp.web.navigation.topNavigationBar();
    +const quick = await sp.web.navigation.quicklaunch();
    +
    +

    For the following examples we will refer to a variable named "nav" that is understood to be one of topNavigationBar or quicklaunch.

    +

    getById

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +const node = await nav.getById(3)();
    +
    +

    add

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +const result = await nav.add("Node Title", "/sites/dev/pages/mypage.aspx", true);
    +
    +const nodeDataRaw = result.data;
    +
    +// request the data from the created node
    +const nodeData = result.node();
    +
    +

    moveAfter

    +

    Places a navigation node after another node in the tree

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +const node1result = await nav.add(`Testing - ${getRandomString(4)} (1)`, url, true);
    +const node2result = await nav.add(`Testing - ${getRandomString(4)} (2)`, url, true);
    +const node1 = await node1result.node();
    +const node2 = await node2result.node();
    +
    +await nav.moveAfter(node1.Id, node2.Id);
    +
    +

    Delete

    +

    Deletes a given node

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +const node1result = await nav.add(`Testing - ${getRandomString(4)}`, url, true);
    +let nodes = await nav();
    +// check we added a node
    +let index = nodes.findIndex(n => n.Id === node1result.data.Id)
    +// index >= 0
    +
    +// delete a node
    +await nav.getById(node1result.data.Id).delete();
    +
    +nodes = await nav();
    +index = nodes.findIndex(n => n.Id === node1result.data.Id)
    +// index = -1
    +
    +

    Update

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +
    +await nav.getById(4).update({
    +    Title: "A new title",
    +});
    +
    +

    Children

    +

    The children property of a Navigation Node represents a collection with all the same properties and methods available on topNavigationBar or quicklaunch.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/navigation";
    +
    +const childrenData = await nav.getById(1).children();
    +
    +// add a child
    +await nav.getById(1).children.add("Title", "Url", true);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/permissions/index.html b/v2/sp/permissions/index.html new file mode 100644 index 000000000..399c98c8d --- /dev/null +++ b/v2/sp/permissions/index.html @@ -0,0 +1,2338 @@ + + + + + + + + + + + + + + + + + + Permissions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp - permissions

    +

    A common task is to determine if a user or the current user has a certain permission level. It is a great idea to check before performing a task such as creating a list to ensure a user can without getting back an error. This allows you to provide a better experience to the user.

    +

    Permissions in SharePoint are assigned to the set of securable objects which include Site, Web, List, and List Item. These are the four level to which unique permissions can be assigned. As such @pnp/sp provides a set of methods defined in the QueryableSecurable class to handle these permissions. These examples all use the Web to get the values, however the methods work identically on all securables.

    +

    Get Role Assignments

    +

    This gets a collection of all the role assignments on a given securable. The property returns a RoleAssignments collection which supports the OData collection operators.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/security";
    +import { Logger } from "@pnp/logging";
    +
    +const roles = await sp.web.roleAssignments();
    +Logger.writeJSON(roles);
    +
    +

    First Unique Ancestor Securable Object

    +

    This method can be used to find the securable parent up the hierarchy that has unique permissions. If everything inherits permissions this will be the Site. If a sub web has unique permissions it will be the web, and so on.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/security";
    +import { Logger } from "@pnp/logging";
    +
    +const obj = await sp.web.firstUniqueAncestorSecurableObject();
    +Logger.writeJSON(obj);
    +
    +

    User Effective Permissions

    +

    This method returns the BasePermissions for a given user or the current user. This value contains the High and Low values for a user on the securable you have queried.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/security";
    +import { Logger } from "@pnp/logging";
    +
    +const perms = await sp.web.getUserEffectivePermissions("i:0#.f|membership|user@site.com");
    +Logger.writeJSON(perms);
    +
    +const perms2 = await sp.web.getCurrentUserEffectivePermissions();
    +Logger.writeJSON(perms2);
    +
    +

    User Has Permissions

    +

    Because the High and Low values in the BasePermission don't obviously mean anything you can use these methods along with the PermissionKind enumeration to check actual rights on the securable.

    +
    import { sp } from "@pnp/sp";
    +import { PermissionKind } from "@pnp/sp/security";
    +
    +const perms = await sp.web.userHasPermissions("i:0#.f|membership|user@site.com", PermissionKind.ApproveItems);
    +console.log(perms);
    +
    +const perms2 = await sp.web.currentUserHasPermissions(PermissionKind.ApproveItems);
    +console.log(perms2);
    +
    +

    Has Permissions

    +

    If you need to check multiple permissions it can be more efficient to get the BasePermissions once and then use the hasPermissions method to check them as shown below.

    +
    import { sp } from "@pnp/sp";
    +import { PermissionKind } from "@pnp/sp/security";
    +
    +const perms = await sp.web.getCurrentUserEffectivePermissions();
    +if (sp.web.hasPermissions(perms, PermissionKind.AddListItems) && sp.web.hasPermissions(perms, PermissionKind.DeleteVersions)) {
    +    // ...
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/profiles/index.html b/v2/sp/profiles/index.html new file mode 100644 index 000000000..f3c5ef899 --- /dev/null +++ b/v2/sp/profiles/index.html @@ -0,0 +1,2806 @@ + + + + + + + + + + + + + + + + + + Profiles - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/profiles

    +

    The profile services allows you to work with the SharePoint User Profile Store.

    +

    Profiles

    +

    Profiles is accessed directly from the root sp object.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/profiles";
    +
    + +
    editProfileLink(): Promise<string>
    +
    +
    const editProfileLink = await sp.profiles.editProfileLink();
    +console.log("My edit profile link =" + editProfileLink);
    +
    +

    Is My People List Public

    +

    Provides a boolean that indicates if the current users "People I'm Following" list is public or not

    +
    isMyPeopleListPublic(): Promise<boolean>
    +
    +
    const isPublic = await sp.profiles.isMyPeopleListPublic();
    +console.log("Is my Following list Public =" + isPubic);
    +
    +

    Find out if the current user is followed by another user

    +

    Provides a boolean that indicates if the current users is followed by a specific user.

    +
    amIFollowedBy(loginName: string): Promise<boolean>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const isFollowedBy = await sp.profiles.amIFollowedBy(loginName);
    +console.log("Is " + loginName + " following me? " + isFollowedBy);
    +
    +

    Find out if I am following a specific user

    +

    Provides a boolean that indicates if the current users is followed by a specific user.

    +
    amIFollowing(loginName: string): Promise<boolean>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const following = await sp.profiles.amIFollowing(loginName);
    +console.log("Am I following " + loginName + "? " + following);
    +
    +

    Get the tags I follow

    +

    Gets the tags the current user is following. Accepts max count, default is 20.

    +
    getFollowedTags(maxCount = 20): Promise<string[]>
    +
    +
    const tags = await sp.profiles.getFollowedTags();
    +console.log(tags);
    +
    +

    Get followers for a specific user

    +

    Gets the people who are following the specified user.

    +
    getFollowersFor(loginName: string): Promise<any[]>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const followers = await sp.profiles.getFollowersFor(loginName);
    +followers.forEach((value) => {
    +  console.log(value);
    +});
    +
    +

    Get followers for the current

    +

    Gets the people who are following the current user.

    +
    myFollowers(): ISharePointQueryableCollection
    +
    +
    const folowers = await sp.profiles.myFollowers();
    +console.log(folowers);
    +
    +

    Get the properties for the current user

    +

    Gets user properties for the current user.

    +
    myProperties(): _SharePointQueryableInstance
    +
    +
    const profile = await sp.profiles.myProperties();
    +console.log(profile.DisplayName);
    +console.log(profile.Email);
    +console.log(profile.Title);
    +console.log(profile.UserProfileProperties.length);
    +
    +// Properties are stored in Key/Value pairs,
    +// so parse into an object called userProperties
    +var props = {};
    +profile.UserProfileProperties.forEach((prop) => {
    +  props[prop.Key] = prop.Value;
    +});
    +profile.userProperties = props;
    +console.log("Account Name: " + profile.userProperties.AccountName);
    +
    +

    Gets people specified user is following

    +
    getPeopleFollowedBy(loginName: string): Promise<any[]>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const folowers = await sp.profiles.getFollowersFor(loginName);
    +followers.forEach((value) => {
    +  console.log(value);
    +});
    +
    +

    Gets properties for a specified user

    +
    getPropertiesFor(loginName: string): Promise<any>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const profile = await sp.profiles.getPropertiesFor(loginName);
    +console.log(profile.DisplayName);
    +console.log(profile.Email);
    +console.log(profile.Title);
    +console.log(profile.UserProfileProperties.length);
    +
    +// Properties are stored in inconvenient Key/Value pairs,
    +// so parse into an object called userProperties
    +var props = {};
    +profile.UserProfileProperties.forEach((prop) => {
    +  props[prop.Key] = prop.Value;
    +});
    +
    +profile.userProperties = props;
    +console.log("Account Name: " + profile.userProperties.AccountName);
    +
    + +

    Gets the 20 most popular hash tags over the past week, sorted so that the most popular tag appears first

    +
    trendingTags(): Promise<IHashTagCollection>
    +
    +
    const tags = await sp.profiles.trendingTags();
    +tags.Items.forEach((tag) => {
    +  console.log(tag);
    +});
    +
    +

    Gets specified user profile property for the specified user

    +
    getUserProfilePropertyFor(loginName: string, propertyName: string): Promise<string>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const propertyName = "AccountName";
    +const property = await sp.profiles.getUserProfilePropertyFor(loginName, propertyName);
    +console.log(property);
    +
    +

    Hide specific user from list of suggested people

    +

    Removes the specified user from the user's list of suggested people to follow.

    +
    hideSuggestion(loginName: string): Promise<void>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +await sp.profiles.hideSuggestion(loginName);
    +
    +

    Is one user following another

    +

    Indicates whether the first user is following the second user. +First parameter is the account name of the user who might be following the followee. +Second parameter is the account name of the user who might be followed by the follower.

    +
    isFollowing(follower: string, followee: string): Promise<boolean>
    +
    +
    const follower = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const followee = "i:0#.f|membership|testuser2@mytenant.onmicrosoft.com";
    +const isFollowing = await sp.profiles.isFollowing(follower, followee);
    +console.log(isFollowing);
    +
    +

    Set User Profile Picture

    +

    Uploads and sets the user profile picture (Users can upload a picture to their own profile only). Not supported for batching. +Accepts the profilePicSource Blob data representing the user's picture in BMP, JPEG, or PNG format of up to 4.76MB.

    +
    setMyProfilePic(profilePicSource: Blob): Promise<void>
    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/profiles";
    +import "@pnp/sp/folders";
    +import "@pnp/sp/files";
    +
    +// get the blob object through a request or from a file input
    +const blob = await sp.web.lists.getByTitle("Documents").rootFolder.files.getByName("profile.jpg").getBlob();
    +
    +await sp.profiles.setMyProfilePic(blob);
    +
    +

    Sets single value User Profile property

    +

    accountName The account name of the user +propertyName Property name +propertyValue Property value

    +
    setSingleValueProfileProperty(accountName: string, propertyName: string, propertyValue: string): Promise<void>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +await sp.profiles.setSingleValueProfileProperty(loginName, "CellPhone", "(123) 555-1212");
    +
    +

    Sets a mult-value User Profile property

    +

    accountName The account name of the user +propertyName Property name +propertyValues Property values

    +
    setMultiValuedProfileProperty(accountName: string, propertyName: string, propertyValues: string[]): Promise<void>
    +
    +
    const loginName = "i:0#.f|membership|testuser@mytenant.onmicrosoft.com";
    +const propertyName = "SPS-Skills";
    +const propertyValues = ["SharePoint", "Office 365", "Architecture", "Azure"];
    +await sp.profiles.setMultiValuedProfileProperty(loginName, propertyName, propertyValues);
    +const profile = await sp.profiles.getPropertiesFor(loginName);
    +var props = {};
    +profile.UserProfileProperties.forEach((prop) => {
    +  props[prop.Key] = prop.Value;
    +});
    +profile.userProperties = props;
    +console.log(profile.userProperties[propertyName]);
    +
    +

    Create Personal Site for specified users

    +

    Provisions one or more users' personal sites. (My Site administrator on SharePoint Online only) +Emails The email addresses of the users to provision sites for

    +
    createPersonalSiteEnqueueBulk(...emails: string[]): Promise<void>
    +
    +
    let userEmails: string[] = ["testuser1@mytenant.onmicrosoft.com", "testuser2@mytenant.onmicrosoft.com"];
    +await sp.profiles.createPersonalSiteEnqueueBulk(userEmails);
    +
    +

    Get the user profile of the owner for the current site

    +
    ownerUserProfile(): Promise<IUserProfile>
    +
    +
    const profile = await sp.profiles.ownerUserProfile();
    +console.log(profile);
    +
    +

    Get the user profile of the current user

    +
    userProfile(): Promise<any>
    +
    +
    const profile = await sp.profiles.userProfile();
    +console.log(profile);
    +
    +

    Create personal site for current user

    +
    createPersonalSite(interactiveRequest = false): Promise<void>
    +
    +
    await sp.profiles.createPersonalSite();
    +
    +

    Make all profile data public or private

    +

    Set the privacy settings for all social data.

    +
    shareAllSocialData(share: boolean): Promise<void>
    +
    +
    await sp.profiles.shareAllSocialData(true);
    +
    +

    Resolve a user or group

    +

    Resolves user or group using specified query parameters

    +
    clientPeoplePickerResolveUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity>
    +
    +
    const result = await sp.profiles.clientPeoplePickerSearchUser({
    +  AllowEmailAddresses: true,
    +  AllowMultipleEntities: false,
    +  MaximumEntitySuggestions: 25,
    +  QueryString: 'John'
    +});
    +console.log(result);
    +
    +

    Search a user or group

    +

    Searches for users or groups using specified query parameters

    +
    clientPeoplePickerSearchUser(queryParams: IClientPeoplePickerQueryParameters): Promise<IPeoplePickerEntity[]>
    +
    +
    const result = await sp.profiles.clientPeoplePickerSearchUser({
    +  AllowEmailAddresses: true,
    +  AllowMultipleEntities: false,
    +  MaximumEntitySuggestions: 25,
    +  QueryString: 'John'
    +});
    +console.log(result);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/regional-settings/index.html b/v2/sp/regional-settings/index.html new file mode 100644 index 000000000..52fc5eb1e --- /dev/null +++ b/v2/sp/regional-settings/index.html @@ -0,0 +1,2394 @@ + + + + + + + + + + + + + + + + + + Regional Settings - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/regional-settings

    +

    The regional settings module helps with managing dates and times across various timezones.

    +

    IRegionalSettings

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import { IRegionalSettings, ITimeZone, ITimeZones, RegionalSettings, TimeZone, TimeZones, } from "@pnp/sp/regional-settings";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/regional-settings";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/regional-settings/web";
    Preset: Allimport { sp, Webs, IWebs } from "@pnp/sp/presets/all";
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/regional-settings/web";
    +
    +// get all the web's regional settings
    +const s = await sp.web.regionalSettings();
    +
    +// select only some settings to return
    +const s2 = await sp.web.regionalSettings.select("DecimalSeparator", "ListSeparator", "IsUIRightToLeft")();
    +
    +

    Installed Languages

    +

    You can get a list of the installed languages in the web.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/regional-settings/web";
    +
    +const s = await sp.web.regionalSettings.getInstalledLanguages();
    +
    +
    +

    The installedLanguages property accessor is deprecated after 2.0.4 in favor of getInstalledLanguages and will be removed in future versions.

    +
    +

    TimeZones

    +

    You can also get information about the selected timezone in the web and all of the defined timezones.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/regional-settings/web";
    +
    +// get the web's configured timezone
    +const s = await sp.web.regionalSettings.timeZone();
    +
    +// select just the Description and Id
    +const s2 = await sp.web.regionalSettings.timeZone.select("Description", "Id")();
    +
    +// get all the timezones
    +const s3 = await sp.web.regionalSettings.timeZones();
    +
    +// get a specific timezone by id
    +// list of ids: https://msdn.microsoft.com/en-us/library/office/jj247008.aspx
    +const s4 = await sp.web.regionalSettings.timeZones.getById(23);
    +const s5 = await s.localTimeToUTC(new Date());
    +
    +// convert a given date from web's local time to UTC time
    +const s6 = await sp.web.regionalSettings.timeZone.localTimeToUTC(new Date());
    +
    +// convert a given date from UTC time to web's local time
    +const s6 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date())
    +const s7 = await sp.web.regionalSettings.timeZone.utcToLocalTime(new Date(2019, 6, 10, 10, 0, 0, 0))
    +
    +

    Title and Description Resources

    +

    Added in 2.0.4

    +

    Some objects allow you to read language specific title information as shown in the following sample. This applies to Web, List, Field, Content Type, and User Custom Actions.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/regional-settings";
    +
    +//
    +// The below methods appears on
    +// - Web
    +// - List
    +// - Field
    +// - ContentType
    +// - User Custom Action
    +//
    +// after you import @pnp/sp/regional-settings
    +//
    +// you can also import just parts of the regional settings:
    +// import "@pnp/sp/regional-settings/web";
    +// import "@pnp/sp/regional-settings/list";
    +// import "@pnp/sp/regional-settings/content-type";
    +// import "@pnp/sp/regional-settings/field";
    +// import "@pnp/sp/regional-settings/user-custom-actions";
    +
    +
    +const title = await sp.web.titleResource("en-us");
    +const title2 = await sp.web.titleResource("de-de");
    +
    +const description = await sp.web.descriptionResource("en-us");
    +const description2 = await sp.web.descriptionResource("de-de");
    +
    +
    +

    You can only read the values through the REST API, not set the value.

    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/related-items/index.html b/v2/sp/related-items/index.html new file mode 100644 index 000000000..a98a74d3c --- /dev/null +++ b/v2/sp/related-items/index.html @@ -0,0 +1,2417 @@ + + + + + + + + + + + + + + + + + + Related Items - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/related-items

    +

    The related items API allows you to add related items to items within a task or workflow list. Related items need to be in the same site collection.

    +

    Setup

    +

    Instead of copying this block of code into each sample, understand that each sample is meant to run with this supporting code to work.

    +
    import { sp, extractWebUrl } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/related-items/web";
    +import "@pnp/sp/lists/web";
    +import "@pnp/sp/items/list";
    +import "@pnp/sp/files/list";
    +import { IList } from "@pnp/sp/lists";
    +import { getRandomString } from "@pnp/core";
    +
    +// setup some lists (or just use existing ones this is just to show the complete process)
    +// we need two lists to use for creating related items, they need to use template 107 (task list)
    +const ler1 = await sp.web.lists.ensure("RelatedItemsSourceList", "", 107);
    +const ler2 = await sp.web.lists.ensure("RelatedItemsTargetList", "", 107);
    +
    +const sourceList = ler1.list;
    +const targetList = ler2.list;
    +
    +const sourceListName = await sourceList.select("Id")().then(r => r.Id);
    +const targetListName = await targetList.select("Id")().then(r => r.Id);
    +
    +// or whatever you need to get the web url, both our example lists are in the same web.
    +const webUrl = sp.web.toUrl();
    +
    +// ...individual samples start here
    +
    + +
    const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
    +
    +

    addSingleLinkToUrl

    +

    This method adds a link to task item based on a url. The list name and item id are to the task item, the url is to the related item/document.

    +
    // get a file's server relative url in some manner, here we add one
    +const file = await sp.web.defaultDocumentLibrary.rootFolder.files.add(`file_${getRandomString(4)}.txt`, "Content", true).then(r => r.data);
    +// add an item or get an item from the task list
    +const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +await sp.web.relatedItems.addSingleLinkToUrl(targetListName, targetItem.Id, file.ServerRelativeUrl);
    +
    +

    addSingleLinkFromUrl

    +

    This method adds a link to task item based on a url. The list name and item id are to related item, the url is to task item to which the related reference is being added. I haven't found a use case for this method.

    + +

    This method allows you to delete a link previously created.

    +
    const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +// add the link
    +await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
    +
    +// delete the link
    +await sp.web.relatedItems.deleteSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
    +
    +

    getRelatedItems

    +

    Gets the related items for an item

    +
    import { IRelatedItem } from "@pnp/sp/related-items";
    +
    +const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +// add a link
    +await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
    +
    +const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +// add a link
    +await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);
    +
    +const items: IRelatedItem[] = await sp.web.relatedItems.getRelatedItems(sourceListName, sourceItem.Id);
    +
    +// items.length === 2
    +
    +

    Related items are defined by the IRelatedItem interface

    +
    export interface IRelatedItem {
    +    ListId: string;
    +    ItemId: number;
    +    Url: string;
    +    Title: string;
    +    WebId: string;
    +    IconUrl: string;
    +}
    +
    +

    getPageOneRelatedItems

    +

    Gets an abbreviated set of related items

    +
    import { IRelatedItem } from "@pnp/sp/related-items";
    +
    +const sourceItem = await sourceList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +const targetItem = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +// add a link
    +await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem.Id, webUrl);
    +
    +const targetItem2 = await targetList.items.add({ Title: `Item ${getRandomString(4)}` }).then(r => r.data);
    +
    +// add a link
    +await sp.web.relatedItems.addSingleLink(sourceListName, sourceItem.Id, webUrl, targetListName, targetItem2.Id, webUrl);
    +
    +const items: IRelatedItem[] = await sp.web.relatedItems.getPageOneRelatedItems(sourceListName, sourceItem.Id);
    +
    +// items.length === 2
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/search/index.html b/v2/sp/search/index.html new file mode 100644 index 000000000..64a74ae05 --- /dev/null +++ b/v2/sp/search/index.html @@ -0,0 +1,2474 @@ + + + + + + + + + + + + + + + + + + Search - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/search

    +

    Using search you can access content throughout your organization in a secure and consistent manner. The library provides support for searching and suggest - as well as some interfaces and helper classes to make building your queries and processing responses easier.

    + +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/search";
    import { ISearchQuery, SearchResults } from "@pnp/sp/search";
    Preset: Allimport { sp, ISearchQuery, SearchResults } from "@pnp/sp/presets/all";
    +

    Search is accessed directly from the root sp object and can take either a string representing the query text, a plain object matching the ISearchQuery interface, or a SearchQueryBuilder instance.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/search";
    +import { ISearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
    +
    +// text search using SharePoint default values for other parameters
    +const results: SearchResults = await sp.search("test");
    +
    +console.log(results.ElapsedTime);
    +console.log(results.RowCount);
    +console.log(results.PrimarySearchResults);
    +
    +
    +// define a search query object matching the ISearchQuery interface
    +const results2: SearchResults = await sp.search(<ISearchQuery>{
    +    Querytext: "test",
    +    RowLimit: 10,
    +    EnableInterleaving: true,
    +});
    +
    +console.log(results2.ElapsedTime);
    +console.log(results2.RowCount);
    +console.log(results2.PrimarySearchResults);
    +
    +// define a query using a builder
    +const builder = SearchQueryBuilder("test").rowLimit(10).enableInterleaving.enableQueryRules.processPersonalFavorites;
    +const results3 = await sp.search(builder);
    +
    +console.log(results3.ElapsedTime);
    +console.log(results3.RowCount);
    +console.log(results3.PrimarySearchResults);
    +
    +

    Search Result Caching

    +

    You can use the searchWithCaching method to enable cache support for your search results this option works with any of the options for providing a query, just replace "search" with "searchWithCaching" in your method chain and gain all the benefits of caching. The second parameter is optional and allows you to specify the cache options

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/search";
    +import { ISearchQuery, SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
    +
    +sp.searchWithCaching({
    +    Querytext: "test",
    +    RowLimit: 10,
    +    EnableInterleaving: true,
    +} as ISearchQuery).then((r: SearchResults) => {
    +
    +    console.log(r.ElapsedTime);
    +    console.log(r.RowCount);
    +    console.log(r.PrimarySearchResults);
    +});
    +
    +// use a query builder
    +const builder = SearchQueryBuilder("test").rowLimit(3);
    +
    +// supply a search query builder and caching options
    +const results2 = await sp.searchWithCaching(builder, { key: "mykey", expiration: dateAdd(new Date(), "month", 1) });
    +
    +console.log(results2.TotalRows);
    +
    +

    Paging with SearchResults.getPage

    +

    Paging is controlled by a start row and page size parameter. You can specify both arguments in your initial query however you can use the getPage method to jump to any page. The second parameter page size is optional and will use the previous RowLimit or default to 10.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/search";
    +import { SearchResults, SearchQueryBuilder } from "@pnp/sp/search";
    +
    +// this will hold our current results
    +let currentResults: SearchResults = null;
    +let page = 1;
    +
    +// triggered on page load or through some other means
    +function onStart() {
    +
    +    // construct our query that will be used throughout the paging process, likely from user input
    +    const q = SearchQueryBuilder("test").rowLimit(5);
    +    const results = await sp.search(q);
    +    currentResults = results; // set the current results
    +    page = 1; // reset page counter
    +    // update UI...
    +}
    +
    +// triggered by an event
    +async function next() {
    +
    +    currentResults = await currentResults.getPage(++page);
    +    // update UI...
    +}
    +
    +// triggered by an event
    +async function prev() {
    +
    +    currentResults = await currentResults.getPage(--page);
    +    // update UI...
    +}
    +
    +

    SearchQueryBuilder

    +

    The SearchQueryBuilder allows you to build your queries in a fluent manner. It also accepts constructor arguments for query text and a base query plain object, should you have a shared configuration for queries in an application you can define them once. The methods and properties match those on the SearchQuery interface. Boolean properties add the flag to the query while methods require that you supply one or more arguments. Also arguments supplied later in the chain will overwrite previous values.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/search";
    +import { SearchQueryBuilder, SearchResults, ISearchQuery } from "@pnp/sp/search";
    +
    +// basic usage
    +let q = SearchQueryBuilder().text("test").rowLimit(4).enablePhonetic;
    +
    +sp.search(q).then(h => { /* ... */ });
    +
    +// provide a default query text at creation
    +let q2 = SearchQueryBuilder("text").rowLimit(4).enablePhonetic;
    +
    +const results: SearchResults = await sp.search(q2);
    +
    +// provide query text and a template for
    +// shared settings across queries that can
    +// be overwritten by individual builders
    +const appSearchSettings: ISearchQuery = {
    +    EnablePhonetic: true,
    +    HiddenConstraints: "reports"
    +};
    +
    +let q3 = SearchQueryBuilder("test", appSearchSettings).enableQueryRules;
    +let q4 = SearchQueryBuilder("financial data", appSearchSettings).enableSorting.enableStemming;
    +const results2 = await sp.search(q3);
    +const results3 = sp.search(q4);
    +
    +

    Search Suggest

    +

    Search suggest works in much the same way as search, except against the suggest end point. It takes a string or a plain object that matches ISuggestQuery.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/search";
    +import { ISuggestQuery, ISuggestResult } from "@pnp/sp/search";
    +
    +const results = await sp.searchSuggest("test");
    +
    +const results2 = await sp.searchSuggest({
    +    querytext: "test",
    +    count: 5,
    +} as ISuggestQuery);
    +
    +

    Search Factory

    +

    You can also configure a search or suggest query against any valid SP url using the factory methods.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/search";
    +import { Search, Suggest } from "@pnp/sp/search";
    +
    +// set the url for search
    +const searcher = Search("https://mytenant.sharepoint.com/sites/dev");
    +
    +// this can accept any of the query types (text, ISearchQuery, or SearchQueryBuilder)
    +const results = await searcher("test");
    +
    +// you can reuse the ISearch instance
    +const results2 = await searcher("another query");
    +
    +// same process works for Suggest
    +const suggester = Suggest("https://mytenant.sharepoint.com/sites/dev");
    +
    +const suggestions = await suggester({ querytext: "test" });
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/security/index.html b/v2/sp/security/index.html new file mode 100644 index 000000000..3ce6057f5 --- /dev/null +++ b/v2/sp/security/index.html @@ -0,0 +1,2432 @@ + + + + + + + + + + + + + + + + + + Security - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/security

    +

    There are four levels where you can break inheritance and assign security: Site, Web, List, Item. All four of these objects share a common set of methods. Because of this we are showing in the examples below usage of these methods for an IList instance, but they apply across all four securable objects. In addition to the shared methods, some types have unique methods which are listed below.

    +
    +

    Site permissions are managed on the root web of the site collection.

    +
    +

    A Note on Selective Imports for Security

    +

    Because the method are shared you can opt to import only the methods for one of the instances.

    +
    import "@pnp/sp/security/web";
    +import "@pnp/sp/security/list";
    +import "@pnp/sp/security/item";
    +
    +

    Possibly useful if you are trying to hyper-optimize for bundle size but it is just as easy to import the whole module:

    +
    import "@pnp/sp/security";
    +
    +

    Securable Methods

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/security/list";
    +import "@pnp/sp/site-users/web";
    +import { IList } from "@pnp/sp/lists";
    +import { PermissionKind } from "@pnp/sp/security";
    +
    +// ensure we have a list
    +const ler = await sp.web.lists.ensure("SecurityTestingList");
    +const list: IList = ler.list;
    +
    +// role assignments (see section below)
    +await list.roleAssignments();
    +
    +// data will represent one of the possible parents Site, Web, or List
    +const data = await list.firstUniqueAncestorSecurableObject();
    +
    +// getUserEffectivePermissions
    +const users = await sp.web.siteUsers.top(1).select("LoginName")();
    +const perms = await list.getUserEffectivePermissions(users[0].LoginName);
    +
    +// getCurrentUserEffectivePermissions
    +const perms2 = list.getCurrentUserEffectivePermissions();
    +
    +// userHasPermissions
    +const v: boolean = list.userHasPermissions(users[0].LoginName, PermissionKind.AddListItems)
    +
    +// currentUserHasPermissions
    +const v2: boolean = list.currentUserHasPermissions(PermissionKind.AddListItems)
    +
    +// breakRoleInheritance
    +await list.breakRoleInheritance();
    +// copy existing permissions
    +await list.breakRoleInheritance(true);
    +// copy existing permissions and reset all child securables to the new permissions
    +await list.breakRoleInheritance(true, true);
    +
    +// resetRoleInheritance
    +await list.resetRoleInheritance();
    +
    +

    Web Specific methods

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/security/web";
    +
    +// role definitions (see section below)
    +const defs = await sp.web.roleDefinitions();
    +
    +

    Role Assignments

    +

    Allows you to list and manipulate the set of role assignments for the given securable. Again we show usage using list, but the examples apply to web and item as well.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/security/web";
    +import "@pnp/sp/site-users/web";
    +import { IList } from "@pnp/sp/lists";
    +import { PermissionKind } from "@pnp/sp/security";
    +
    +// ensure we have a list
    +const ler = await sp.web.lists.ensure("SecurityTestingList");
    +const list: IList = ler.list;
    +
    +// list role assignments
    +const assignments = await list.roleAssignments();
    +
    +// add a role assignment
    +const defs = await sp.web.roleDefinitions();
    +const user = await sp.web.currentUser();
    +const r = await list.roleAssignments.add(user.Id, defs[0].Id);
    +
    +// remove a role assignment
    +const ras = await list.roleAssignments();
    +// filter/find the role assignment you want to remove
    +// here we just grab the first
    +const ra = ras.find(v => true);
    +const r = await list.roleAssignments.remove(ra.Id);
    +
    +// read role assignment info
    +const info = await list.roleAssignments.getById(ra.Id)();
    +
    +// get the groups
    +const info2 = await list.roleAssignments.getById(ra.Id).groups();
    +
    +// get the bindings
    +const info3 = await list.roleAssignments.getById(ra.Id).bindings();
    +
    +// delete a role assignment (same as remove)
    +const ras = await list.roleAssignments();
    +// filter/find the role assignment you want to remove
    +// here we just grab the first
    +const ra = ras.find(v => true);
    +
    +// delete it
    +await list.roleAssignments.getById(ra.Id).delete();
    +
    +

    Role Definitions

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/security/web";
    +
    +// read role definitions
    +const defs = await sp.web.roleDefinitions();
    +
    +// get by id
    +const def = await sp.web.roleDefinitions.getById(5)();
    +const def = await sp.web.roleDefinitions.getById(5).select("Name", "Order")();
    +
    +// get by name
    +const def = await sp.web.roleDefinitions.getByName("Full Control")();
    +const def = await sp.web.roleDefinitions.getByName("Full Control").select("Name", "Order")();
    +
    +// get by type
    +const def = await sp.web.roleDefinitions.getByName(5)();
    +const def = await sp.web.roleDefinitions.getByName(5).select("Name", "Order")();
    +
    +// add
    +// name The new role definition's name
    +// description The new role definition's description
    +// order The order in which the role definition appears
    +// basePermissions The permissions mask for this role definition
    +const rdar = await sp.web.roleDefinitions.add("title", "description", 99, { High: 1, Low: 2 });
    +
    +
    +
    +// the following methods work on a single role def, you can use any of the three getBy methods, here we use getById as an example
    +
    +// delete
    +await sp.web.roleDefinitions.getById(5).delete();
    +
    +// update
    +const res = sp.web.roleDefinitions.getById(5).update({ Name: "New Name" });
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/sharing/index.html b/v2/sp/sharing/index.html new file mode 100644 index 000000000..8b41aadeb --- /dev/null +++ b/v2/sp/sharing/index.html @@ -0,0 +1,2589 @@ + + + + + + + + + + + + + + + + + + Sharing - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    + +
    + + +
    +
    + + + + + + + +

    @pnp/sp/sharing

    +
    +

    Note: This API is still considered "beta" meaning it may change and some behaviors may differ across tenants by version. It is also supported only in SharePoint Online.

    +
    +

    One of the newer abilities in SharePoint is the ability to share webs, files, or folders with both internal and external folks. It is important to remember that these settings are managed at the tenant level and ? override anything you may supply as an argument to these methods. If you receive an InvalidOperationException when using these methods please check your tenant sharing settings to ensure sharing is not blocked before ?submitting an issue.

    +

    Imports

    +

    In previous versions of this library the sharing methods were part of the inheritance stack for SharePointQueryable objects. Starting with v2 this is no longer the case and they are now selectively importable. There are four objects within the SharePoint hierarchy that support sharing: Item, File, Folder, and Web. You can import the sharing methods for all of them, or for individual objects.

    +

    Import All

    +

    To import and attach the sharing methods to all four of the sharable types include all of the sharing sub module:

    +
    import "@pnp/sp/sharing";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +import { sp } from "@pnp/sp";
    +
    +const user = await sp.web.siteUsers.getByEmail("user@site.com")();
    +const r = await sp.web.shareWith(user.LoginName);
    +
    +

    Selective Import

    +

    Import only the web's sharing methods into the library

    +
    import "@pnp/sp/sharing/web";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +import { sp } from "@pnp/sp";
    +
    +const user = await sp.web.siteUsers.getByEmail("user@site.com")();
    +const r = await sp.web.shareWith(user.LoginName);
    +
    + +

    Applies to: Item, Folder, File

    +

    Creates a sharing link for the given resource with an optional expiration.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import { SharingLinkKind, IShareLinkResponse } from "@pnp/sp/sharing";
    +import { dateAdd } from "@pnp/core";
    +
    +const result = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView);
    +
    +console.log(JSON.stringify(result, null, 2));
    +
    +
    +const result2 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").getShareLink(SharingLinkKind.AnonymousView, dateAdd(new Date(), "day", 5));
    +
    +console.log(JSON.stringify(result2, null, 2));
    +
    +

    shareWith

    +

    Applies to: Item, Folder, File, Web

    +

    Shares the given resource with the specified permissions (View or Edit) and optionally sends an email to the users. You can supply a single string for the loginnames parameter or an array of loginnames. The folder method takes an optional parameter "shareEverything" which determines if the shared permissions are pushed down to all items in the folder, even those with unique permissions.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import "@pnp/sp/folders/web";
    +import "@pnp/sp/files/web";
    +import { ISharingResult, SharingRole } from "@pnp/sp/sharing";
    +
    +const result = await sp.web.shareWith("i:0#.f|membership|user@site.com");
    +
    +console.log(JSON.stringify(result, null, 2));
    +
    +// Share and allow editing
    +const result2 = await sp.web.shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit);
    +
    +console.log(JSON.stringify(result2, null, 2));
    +
    +
    +// share folder
    +const result3 = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/folder1").shareWith("i:0#.f|membership|user@site.com");
    +
    +// Share folder with edit permissions, and provide params for requireSignin and propagateAcl (apply to all children)
    +await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit, true, true);
    +
    +// Share a file
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com");
    +
    +// Share a file with edit permissions
    +await sp.web.getFileByServerRelativeUrl("/sites/dev/Shared Documents/test.txt").shareWith("i:0#.f|membership|user@site.com", SharingRole.Edit);
    +
    +

    shareObject & shareObjectRaw

    +

    Applies to: Web

    +

    Allows you to share any shareable object in a web by providing the appropriate parameters. These two methods differ in that shareObject will try and fix up your query based on the supplied parameters where shareObjectRaw will send your supplied json object directly to the server. The later method is provided for the greatest amount of flexibility.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import { ISharingResult, SharingRole } from "@pnp/sp/sharing";
    +
    +// Share an object in this web
    +const result = await sp.web.shareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt", "i:0#.f|membership|user@site.com", SharingRole.View);
    +
    +// Share an object with all settings available
    +await sp.web.shareObjectRaw({
    +    url: "https://mysite.sharepoint.com/sites/dev/Docs/test.txt",
    +    peoplePickerInput: [{ Key: "i:0#.f|membership|user@site.com" }],
    +    roleValue: "role: 1973741327",
    +    groupId: 0,
    +    propagateAcl: false,
    +    sendEmail: true,
    +    includeAnonymousLinkInEmail: false,
    +    emailSubject: "subject",
    +    emailBody: "body",
    +    useSimplifiedRoles: true,
    +});
    +
    +

    unshareObject

    +

    Applies to: Web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import { ISharingResult } from "@pnp/sp/sharing";
    +
    +const result = await sp.web.unshareObject("https://mysite.sharepoint.com/sites/dev/Docs/test.txt");
    +
    +

    checkSharingPermissions

    +

    Applies to: Item, Folder, File

    +

    Checks Permissions on the list of Users and returns back role the users have on the Item.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing/folders";
    +import "@pnp/sp/folders/web";
    +import { SharingEntityPermission } from "@pnp/sp/sharing";
    +
    +// check the sharing permissions for a folder
    +const perms = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").checkSharingPermissions([{ alias: "i:0#.f|membership|user@site.com" }]);
    +
    +

    getSharingInformation

    +

    Applies to: Item, Folder, File

    +

    Get Sharing Information.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import "@pnp/sp/folders";
    +import { ISharingInformation } from "@pnp/sp/sharing";
    +
    +// Get the sharing information for a folder
    +const info = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getSharingInformation();
    +
    +

    getObjectSharingSettings

    +

    Applies to: Item, Folder, File

    +

    Gets the sharing settings

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import "@pnp/sp/folders";
    +import { IObjectSharingSettings } from "@pnp/sp/sharing";
    +
    +// Gets the sharing object settings
    +const settings: IObjectSharingSettings = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").getObjectSharingSettings();
    +
    +

    unshare

    +

    Applies to: Item, Folder, File

    +

    Unshares a given resource

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import "@pnp/sp/folders";
    +import { ISharingResult } from "@pnp/sp/sharing";
    +
    +const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshare();
    +
    +

    deleteSharingLinkByKind

    +

    Applies to: Item, Folder, File

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import "@pnp/sp/folders";
    +import { ISharingResult, SharingLinkKind } from "@pnp/sp/sharing";
    +
    +const result: ISharingResult = await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").deleteSharingLinkByKind(SharingLinkKind.AnonymousEdit);
    +
    + +

    Applies to: Item, Folder, File

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sharing";
    +import "@pnp/sp/folders";
    +import { SharingLinkKind } from "@pnp/sp/sharing";
    +
    +await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit);
    +
    +// specify the sharing link id if available
    +await sp.web.getFolderByServerRelativeUrl("/sites/dev/Shared Documents/test").unshareLink(SharingLinkKind.AnonymousEdit, "12345");
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/site-designs/index.html b/v2/sp/site-designs/index.html new file mode 100644 index 000000000..031f075fa --- /dev/null +++ b/v2/sp/site-designs/index.html @@ -0,0 +1,2414 @@ + + + + + + + + + + + + + + + + + + Site Designs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/site-designs

    +

    You can create site designs to provide reusable lists, themes, layouts, pages, or custom actions so that your users can quickly build new SharePoint sites with the features they need. +Check out SharePoint site design and site script overview for more information.

    +

    Site Designs

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/site-designs";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    Create a new site design

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-designs";
    +
    +// WebTemplate: 64 Team site template, 68 Communication site template
    +const siteDesign = await sp.siteDesigns.createSiteDesign({
    +    SiteScriptIds: ["884ed56b-1aab-4653-95cf-4be0bfa5ef0a"],
    +    Title: "SiteDesign001",
    +    WebTemplate: "64",
    +});
    +
    +console.log(siteDesign.Title);
    +
    +

    Applying a site design to a site

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-designs";
    +
    +// Limited to 30 actions in a site script, but runs synchronously
    +await sp.siteDesigns.applySiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8","https://contoso.sharepoint.com/sites/teamsite-pnpjs001");
    +
    +// Better use the following method for 300 actions in a site script
    +const task = await sp.web.addSiteDesignTask("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +
    +

    Retrieval

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-designs";
    +
    +// Retrieving all site designs
    +const allSiteDesigns = await sp.siteDesigns.getSiteDesigns();
    +console.log(`Total site designs: ${allSiteDesigns.length}`);
    +
    +// Retrieving a single site design by Id
    +const siteDesign = await sp.siteDesigns.getSiteDesignMetadata("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +console.log(siteDesign.Title);
    +
    +

    Update and delete

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-designs";
    +
    +// Update
    +const updatedSiteDesign = await sp.siteDesigns.updateSiteDesign({ Id: "75b9d8fe-4381-45d9-88c6-b03f483ae6a8", Title: "SiteDesignUpdatedTitle001" });
    +
    +// Delete
    +await sp.siteDesigns.deleteSiteDesign("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +
    +

    Setting Rights/Permissions

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-designs";
    +
    +// Get
    +const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +console.log(rights.length > 0 ? rights[0].PrincipalName : "");
    +
    +// Grant
    +await sp.siteDesigns.grantSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
    +
    +// Revoke
    +await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", ["user@contoso.onmicrosoft.com"]);
    +
    +// Reset all view rights
    +const rights = await sp.siteDesigns.getSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +await sp.siteDesigns.revokeSiteDesignRights("75b9d8fe-4381-45d9-88c6-b03f483ae6a8", rights.map(u => u.PrincipalName));
    +
    +

    Get a history of site designs that have run on a web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-designs";
    +
    +const runs = await sp.web.getSiteDesignRuns();
    +const runs2 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite");
    +
    +// Get runs specific to a site design
    +const runs3 = await sp.web.getSiteDesignRuns("75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +const runs4 = await sp.siteDesigns.getSiteDesignRun("https://TENANT.sharepoint.com/sites/mysite", "75b9d8fe-4381-45d9-88c6-b03f483ae6a8");
    +
    +// For more information about the site script actions
    +const runStatus = await sp.web.getSiteDesignRunStatus(runs[0].ID);
    +const runStatus2 = await sp.siteDesigns.getSiteDesignRunStatus("https://TENANT.sharepoint.com/sites/mysite", runs[0].ID);
    +
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/site-groups/index.html b/v2/sp/site-groups/index.html new file mode 100644 index 000000000..24bed7a48 --- /dev/null +++ b/v2/sp/site-groups/index.html @@ -0,0 +1,2504 @@ + + + + + + + + + + + + + + + + + + Site Groups - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/site-groups

    +

    The site groups module provides methods to manage groups for a sharepoint site.

    +

    ISiteGroups

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-groups";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-groups/web";
    Preset: Allimport {sp, SiteGroups } from "@pnp/sp/presets/all";
    +

    Get all site groups

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups/web";
    +
    +// gets all site groups of the web
    +const groups = await sp.web.siteGroups();
    +
    +

    Get the associated groups of a web

    +

    You can get the associated Owner, Member and Visitor groups of a web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups/web";
    +
    +// Gets the associated visitors group of a web
    +const visitorGroup = await sp.web.associatedVisitorGroup();
    +
    +// Gets the associated members group of a web
    +const memberGroup = await sp.web.associatedMemberGroup();
    +
    +// Gets the associated owners group of a web
    +const ownerGroup = await sp.web.associatedOwnerGroup();
    +
    +
    +

    Create the default associated groups for a web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups/web";
    +
    +// Breaks permission inheritance and creates the default associated groups for the web
    +
    +// Login name of the owner
    +const owner1 = "owner@example.onmicrosoft.com";
    +
    +// Specify true, the permissions should be copied from the current parent scope, else false
    +const copyRoleAssignments = false;
    +
    +// Specify true to make all child securable objects inherit role assignments from the current object
    +const clearSubScopes = true;
    +
    +await sp.web.createDefaultAssociatedGroups("PnP Site", owner1, copyRoleAssignments, clearSubScopes);
    +
    +

    Create a new site group

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups/web";
    +
    +// Creates a new site group with the specified title
    +await sp.web.siteGroups.add({"Title":"new group name"});
    +
    +

    ISiteGroup

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-groups";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-groups/web";
    Preset: Allimport {sp, SiteGroups, SiteGroup } from "@pnp/sp/presets/all";
    +

    Getting and updating the groups of a sharepoint web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups";
    +
    +// get the group using a group id
    +const groupID = 33;
    +let grp = await sp.web.siteGroups.getById(groupID)();
    +
    +// get the group using the group's name
    +const groupName = "ClassicTeam Visitors";
    +grp = await sp.web.siteGroups.getByName(groupName)();
    +
    +// update a group
    +await sp.web.siteGroups.getById(groupID).update({"Title": "New Group Title"});
    +
    +// delete a group from the site using group id
    +await sp.web.siteGroups.removeById(groupID);
    +
    +// delete a group from the site using group name
    +await sp.web.siteGroups.removeByLoginName(groupName);
    +
    +

    Getting all users of a group

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups";
    +
    +// get all users of group
    +const groupID = 7;
    +const users = await sp.web.siteGroups.getById(groupID).users();
    +
    +

    Updating the owner of a site group

    +

    Unfortunately for now setting the owner of a group as another or same SharePoint group is currently unsupported in REST. Setting the owner as a user is supported.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-groups";
    +
    +// Update the owner with a user id
    +await sp.web.siteGroups.getById(7).setUserAsOwner(4);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/site-scripts/index.html b/v2/sp/site-scripts/index.html new file mode 100644 index 000000000..96c986f93 --- /dev/null +++ b/v2/sp/site-scripts/index.html @@ -0,0 +1,2433 @@ + + + + + + + + + + + + + + + + + + Site Scripts - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/site-scripts

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/site-scripts";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    Create a new site script

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-scripts";
    +
    +const sitescriptContent = {
    +    "$schema": "schema.json",
    +    "actions": [
    +        {
    +            "themeName": "Theme Name 123",
    +            "verb": "applyTheme",
    +        },
    +    ],
    +    "bindata": {},
    +    "version": 1,
    +};
    +
    +const siteScript = await sp.siteScripts.createSiteScript("Title", "description", sitescriptContent);
    +
    +console.log(siteScript.Title);
    +
    +

    Retrieval

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-scripts";
    +
    +// Retrieving all site scripts
    +const allSiteScripts = await sp.siteScripts.getSiteScripts();
    +console.log(allSiteScripts.length > 0 ? allSiteScripts[0].Title : "");
    +
    +// Retrieving a single site script by Id
    +const siteScript = await sp.siteScripts.getSiteScriptMetadata("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
    +console.log(siteScript.Title);
    +
    +

    Update and delete

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-scripts";
    +
    +// Update
    +const updatedSiteScript = await sp.siteScripts.updateSiteScript({ Id: "884ed56b-1aab-4653-95cf-4be0bfa5ef0a", Title: "New Title" });
    +console.log(updatedSiteScript.Title);
    +
    +// Delete
    +await sp.siteScripts.deleteSiteScript("884ed56b-1aab-4653-95cf-4be0bfa5ef0a");
    +
    +

    Get site script from a list

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-scripts";
    +
    +// Using the absolute URL of the list
    +const ss = await sp.siteScripts.getSiteScriptFromList("https://TENANT.sharepoint.com/Lists/mylist");
    +
    +// Using the PnPjs web object to fetch the site script from a specific list
    +const ss2 = await sp.web.lists.getByTitle("mylist").getSiteScript();
    +
    +

    Get site script from a web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-scripts";
    +
    +const extractInfo = {
    +    IncludeBranding: true,
    +    IncludeLinksToExportedItems: true,
    +    IncludeRegionalSettings: true,
    +    IncludeSiteExternalSharingCapability: true,
    +    IncludeTheme: true,
    +    IncludedLists: ["Lists/MyList"]
    +};
    +
    +const ss = await sp.siteScripts.getSiteScriptFromWeb("https://TENANT.sharepoint.com/sites/mysite", extractInfo);
    +
    +// Using the PnPjs web object to fetch the site script from a specific web
    +const ss2 = await sp.web.getSiteScript(extractInfo);
    +
    +

    Execute Site Script Action

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/site-scripts";
    +
    +const siteScript = "your site script action...";
    +
    +const ss = await sp.siteScripts.executeSiteScriptAction(siteScript);
    +
    +

    Execute site script for a specific web

    +
    import { sp } from "@pnp/sp";
    +import { SiteScripts } "@pnp/sp/site-scripts";
    +
    +const siteScript = "your site script action...";
    +
    +const scriptService = SiteScripts("https://absolute/url/to/web");
    +
    +const ss = await scriptService.executeSiteScriptAction(siteScript);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/site-users/index.html b/v2/sp/site-users/index.html new file mode 100644 index 000000000..d2b35af84 --- /dev/null +++ b/v2/sp/site-users/index.html @@ -0,0 +1,2641 @@ + + + + + + + + + + + + + + + + + + Site Users - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/site-users

    +

    The site users module provides methods to manage users for a sharepoint site.

    +

    ISiteUsers

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-users";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-users/web";
    Preset: Allimport {sp, SiteUsers } from "@pnp/sp/presets/all";
    +

    Get all site user

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +const users = await sp.web.siteUsers();
    +
    +

    Get Current user

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +let user = await sp.web.currentUser();
    +
    +

    Get user by id

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +const id = 6;
    +user = await sp.web.getUserById(id);
    +
    +

    Ensure user

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +const username = "usernames@microsoft.com";
    +result = await sp.web.ensureUser(username);
    +
    +

    ISiteUser

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-users";
    Selective 3import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/site-users/web";
    Preset: Allimport {sp, SiteUsers, SiteUser } from "@pnp/sp/presets/all";
    +

    Get user Groups

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +let groups = await sp.web.currentUser.groups();
    +
    +

    Add user to Site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +const user = await sp.web.ensureUser("userLoginname")
    +const users = await sp.web.siteUsers;
    +
    +await users.add(user.data.LoginName);
    +
    +

    Get user

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +// get user object by id
    +const user = await sp.web.siteUsers.getById(6);
    +
    +//get user object by Email
    +const user = await sp.web.siteUsers.getByEmail("user@mail.com");
    +
    +//get user object by LoginName
    +const user = await sp.web.siteUsers.getByLoginName("userLoginName");
    +
    +

    Update user

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +let userProps = await sp.web.currentUser();
    +userProps.Title = "New title";
    +await sp.web.currentUser.update(userProps);
    +
    +

    Remove user

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/site-users/web";
    +
    +// remove user by id
    +await sp.web.siteUsers.removeById(6);
    +
    +// remove user by LoginName
    +await sp.web.siteUsers.removeByLoginName(6);
    +
    +

    ISiteUserProps

    +

    User properties:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    Property NameTypeDescription
    EmailstringContains Site user email
    IdNumberContains Site user Id
    IsHiddenInUIBooleanSite user IsHiddenInUI
    IsShareByEmailGuestUserbooleanSite user is external user
    IsSiteAdminBooleanDescribes if Site user Is Site Admin
    LoginNamestringSite user LoginName
    PrincipalTypenumberSite user Principal type
    TitlestringSite user Title
    +
    interface ISiteUserProps {
    +
    +    /**
    +     * Contains Site user email
    +     *
    +     */
    +    Email: string;
    +
    +    /**
    +     * Contains Site user Id
    +     *
    +     */
    +    Id: number;
    +
    +    /**
    +     * Site user IsHiddenInUI
    +     *
    +     */
    +    IsHiddenInUI: boolean;
    +
    +    /**
    +     * Site user IsShareByEmailGuestUser
    +     *
    +     */
    +    IsShareByEmailGuestUser: boolean;
    +
    +    /**
    +     * Describes if Site user Is Site Admin
    +     *
    +     */
    +    IsSiteAdmin: boolean;
    +
    +    /**
    +     * Site user LoginName
    +     *
    +     */
    +    LoginName: string;
    +
    +    /**
    +     * Site user Principal type
    +     *
    +     */
    +    PrincipalType: number | PrincipalType;
    +
    +    /**
    +     * Site user Title
    +     *
    +     */
    +    Title: string;
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/sites/index.html b/v2/sp/sites/index.html new file mode 100644 index 000000000..6c61ef4bc --- /dev/null +++ b/v2/sp/sites/index.html @@ -0,0 +1,2727 @@ + + + + + + + + + + + + + + + + + + Sites - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/site - Site properties

    +

    Site collection are one of the fundamental entry points while working with SharePoint. Sites serve as container for webs, lists, features and other entity types.

    +

    Get context information for the current site collection

    +

    Using the library, you can get the context information of the current site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +import { IContextInfo } from "@pnp/sp/sites";
    +
    +const oContext: IContextInfo = await sp.site.getContextInfo();
    +console.log(oContext.FormDigestValue);
    +
    +

    Get document libraries of a web

    +

    Using the library, you can get a list of the document libraries present in the a given web.

    +

    Note: Works only in SharePoint online

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +import { IDocumentLibraryInformation } from "@pnp/sp/sites";
    +
    +const docLibs: IDocumentLibraryInformation[] = await sp.site.getDocumentLibraries("https://tenant.sharepoint.com/sites/test/subsite");
    +
    +//we got the array of document library information
    +docLibs.forEach((docLib: IDocumentLibraryInformation) => {
    +    // do something with each library
    +});
    +
    +

    Open Web By Id

    +

    Because this method is a POST request you can chain off it directly. You will get back the full web properties in the data property of the return object. You can also chain directly off the returned Web instance on the web property.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +
    +const w = await sp.site.openWebById("111ca453-90f5-482e-a381-cee1ff383c9e");
    +
    +//we got all the data from the web as well
    +console.log(w.data);
    +
    +// we can chain
    +const w2 = await w.web.select("Title")();
    +
    +

    Get site collection url from page

    +

    Using the library, you can get the site collection url by providing a page url

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +
    +const d: string = await sp.site.getWebUrlFromPageUrl("https://tenant.sharepoint.com/sites/test/Pages/test.aspx");
    +
    +console.log(d); //https://tenant.sharepoint.com/sites/test
    +
    +

    Access the root web

    +

    There are two methods to access the root web. The first, using the rootWeb property, is best for directly accessing information about that web. If you want to chain multiple operations off of the web, better to use the getRootWeb method that will ensure the web instance is created using its own Url vs. "_api/sites/rootweb" which does not work for all operations.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +
    +// use for rootweb information access
    +const rootwebData = await sp.site.rootWeb();
    +
    +// use for chaining
    +const rootweb = await sp.site.getRootWeb();
    +const listData = await rootWeb.lists.getByTitle("MyList")();
    +
    +

    Create a modern communication site

    +

    Note: Works only in SharePoint online

    +

    Creates a modern communication site.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyTypeRequiredDescription
    TitlestringyesThe title of the site to create.
    lcidnumberyesThe default language to use for the site.
    shareByEmailEnabledbooleanyesIf set to true, it will enable sharing files via Email. By default it is set to false
    urlstringyesThe fully qualified URL (e.g. https://yourtenant.sharepoint.com/sites/mysitecollection) of the site.
    descriptionstringnoThe description of the communication site.
    classificationstringnoThe Site classification to use. For instance "Contoso Classified". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information
    siteDesignIdstringnoThe Guid of the site design to be used.
    You can use the below default OOTB GUIDs:
    Topic: null
    Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767
    Blank: f6cc5403-0d63-442e-96c0-285923709ffc
    hubSiteIdstringnoThe Guid of the already existing Hub site
    OwnerstringnoRequired when using app-only context. Owner principal name e.g. user@tenant.onmicrosoft.com
    +
    
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +
    +const result = await sp.site.createCommunicationSite(
    +            "Title",
    +            1033,
    +            true,
    +            "https://tenant.sharepoint.com/sites/commSite",
    +            "Description",
    +            "HBI",
    +            "f6cc5403-0d63-442e-96c0-285923709ffc",
    +            "a00ec589-ea9f-4dba-a34e-67e78d41e509",
    +            "user@TENANT.onmicrosoft.com");
    +
    +
    +

    Create from Props

    +

    You may need to supply additional parameters such as WebTemplate, to do so please use the createCommunicationSiteFromProps method.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sites";
    +
    +// in this case you supply a single struct deinfing the creation props
    +const result = await sp.site.createCommunicationSiteFromProps({
    +  Owner: "patrick@three18studios.com",
    +  Title: "A Test Site",
    +  Url: "https://{tenant}.sharepoint.com/sites/commsite2",
    +  WebTemplate: "STS#3",
    +});
    +
    +

    Create a modern team site

    +

    Note: Works only in SharePoint online. It wont work with App only tokens

    +

    Creates a modern team site backed by O365 group.

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    PropertyTypeRequiredDescription
    displayNamestringyesThe title/displayName of the site to be created.
    aliasstringyesAlias of the underlying Office 365 Group.
    isPublicbooleanyesDefines whether the Office 365 Group will be public (default), or private.
    lcidnumberyesThe language to use for the site. If not specified will default to English (1033).
    descriptionstringnoThe description of the modern team site.
    classificationstringnoThe Site classification to use. For instance "Contoso Classified". See https://www.youtube.com/watch?v=E-8Z2ggHcS0 for more information
    ownersstring array (string[])noThe Owners of the site to be created
    hubSiteIdstringnoThe Guid of the already existing Hub site
    siteDesignIdstringnoThe Guid of the site design to be used.
    You can use the below default OOTB GUIDs:
    Topic: null
    Showcase: 6142d2a0-63a5-4ba0-aede-d9fefca2c767
    Blank: f6cc5403-0d63-442e-96c0-285923709ffc
    +
    
    +import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +
    +const result = await sp.site.createModernTeamSite(
    +        "displayName",
    +        "alias",
    +        true,
    +        1033,
    +        "description",
    +        "HBI",
    +        ["user1@tenant.onmicrosoft.com","user2@tenant.onmicrosoft.com","user3@tenant.onmicrosoft.com"],
    +        "a00ec589-ea9f-4dba-a34e-67e78d41e509",
    +        "f6cc5403-0d63-442e-96c0-285923709ffc"
    +        );
    +
    +console.log(d);
    +
    +

    Create from Props

    +

    You may need to supply additional parameters, to do so please use the createModernTeamSiteFromProps method.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/sites";
    +
    +// in this case you supply a single struct deinfing the creation props
    +const result = await sp.site.createModernTeamSiteFromProps({
    +  alias: "JenniferGarner",
    +  displayName: "A Test Site",
    +  owners: ["patrick@three18studios.com"],
    +});
    +
    +

    Delete a site collection

    +

    Using the library, you can delete a specific site collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sites";
    +import { Site } from "@pnp/sp/sites";
    +
    +// Delete the current site
    +await sp.site.delete();
    +
    +// Specify which site to delete
    +const siteUrl = "https://tenant.sharepoint.com/sites/subsite";
    +const site2 = Site(siteUrl);
    +await site2.delete();
    +
    +

    Check if a Site Collection Exists

    +

    Using the library, you can check if a specific site collection exist or not on your tenant

    +
    import { sp } from "@pnp/sp";
    +
    +// Specify which site to verify
    +const siteUrl = "https://tenant.sharepoint.com/sites/subsite";
    +const exists = sp.site.exists(siteUrl);
    +console.log(exists);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/social/index.html b/v2/sp/social/index.html new file mode 100644 index 000000000..9c01b9177 --- /dev/null +++ b/v2/sp/social/index.html @@ -0,0 +1,2517 @@ + + + + + + + + + + + + + + + + + + Social - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/ - social

    +

    Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import "@pnp/sp/social";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    The social API allows you to track followed sites, people, and docs. Note, many of these methods only work with the context of a logged in user, and not +with app-only permissions.

    +

    getFollowedSitesUri

    +

    Gets a URI to a site that lists the current user's followed sites.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/social";
    +
    +const uri = await sp.social.getFollowedSitesUri();
    +
    +

    getFollowedDocumentsUri

    +

    Gets a URI to a site that lists the current user's followed documents.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/social";
    +
    +const uri = await sp.social.getFollowedDocumentsUri();
    +
    +

    follow

    +

    Makes the current user start following a user, document, site, or tag

    +
    import { sp } from "@pnp/sp";
    +import { SocialActorType } from "@pnp/sp/social";
    +
    +// follow a site
    +const r1 = await sp.social.follow({
    +    ActorType: SocialActorType.Site,
    +    ContentUri: "htts://tenant.sharepoint.com/sites/site",
    +});
    +
    +// follow a person
    +const r2 = await sp.social.follow({
    +    AccountName: "i:0#.f|membership|person@tenant.com",
    +    ActorType: SocialActorType.User,
    +});
    +
    +// follow a doc
    +const r3 = await sp.social.follow({
    +    ActorType: SocialActorType.Document,
    +    ContentUri: "https://tenant.sharepoint.com/sites/dev/SitePages/Test.aspx",
    +});
    +
    +// follow a tag
    +// You need the tag GUID to start following a tag.
    +// You can't get the GUID by using the REST service, but you can use the .NET client object model or the JavaScript object model.
    +// See How to get a tag's GUID based on the tag's name by using the JavaScript object model.
    +// https://docs.microsoft.com/en-us/sharepoint/dev/general-development/follow-content-in-sharepoint#bk_getTagGuid
    +const r4 = await sp.social.follow({
    +    ActorType: SocialActorType.Tag,
    +    TagGuid: "19a4a484-c1dc-4bc5-8c93-bb96245ce928",
    +});
    +
    +

    isFollowed

    +

    Indicates whether the current user is following a specified user, document, site, or tag

    +
    import { sp } from "@pnp/sp";
    +import { SocialActorType } from "@pnp/sp/social";
    +
    +// pass the same social actor struct as shown in follow example for each type
    +const r = await sp.social.isFollowed({
    +    AccountName: "i:0#.f|membership|person@tenant.com",
    +    ActorType: SocialActorType.User,
    +});
    +
    +

    stopFollowing

    +

    Makes the current user stop following a user, document, site, or tag

    +
    import { sp } from "@pnp/sp";
    +import { SocialActorType } from "@pnp/sp/social";
    +
    +// pass the same social actor struct as shown in follow example for each type
    +const r = await sp.social.stopFollowing({
    +    AccountName: "i:0#.f|membership|person@tenant.com",
    +    ActorType: SocialActorType.User,
    +});
    +
    +

    my

    +

    get

    +

    Gets this user's social information

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/social";
    +
    +const r = await sp.social.my();
    +
    +

    followed

    +

    Gets users, documents, sites, and tags that the current user is following based on the supplied flags.

    +
    import { sp } from "@pnp/sp";
    +import { SocialActorType } from "@pnp/sp/social";
    +
    +// get all the followed documents
    +const r1 = await sp.social.my.followed(SocialActorTypes.Document);
    +
    +// get all the followed documents and sites
    +const r2 = await sp.social.my.followed(SocialActorTypes.Document | SocialActorTypes.Site);
    +
    +// get all the followed sites updated in the last 24 hours
    +const r3 = await sp.social.my.followed(SocialActorTypes.Site | SocialActorTypes.WithinLast24Hours);
    +
    +

    followedCount

    +

    Works as followed but returns on the count of actors specified by the query

    +
    import { sp } from "@pnp/sp";
    +import { SocialActorType } from "@pnp/sp/social";
    +
    +// get the followed documents count
    +const r = await sp.social.my.followedCount(SocialActorTypes.Document);
    +
    +

    followers

    +

    Gets the users who are following the current user.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/social";
    +
    +// get the followed documents count
    +const r = await sp.social.my.followers();
    +
    +

    suggestions

    +

    Gets users who the current user might want to follow.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/social";
    +
    +// get the followed documents count
    +const r = await sp.social.my.suggestions();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/sp-utilities-utility/index.html b/v2/sp/sp-utilities-utility/index.html new file mode 100644 index 000000000..571ffbbe8 --- /dev/null +++ b/v2/sp/sp-utilities-utility/index.html @@ -0,0 +1,2460 @@ + + + + + + + + + + + + + + + + + + SP.Utilities.Utility - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/utilities

    +

    Through the REST api you are able to call a subset of the SP.Utilities.Utility methods. We have explicitly defined some of these methods and provided a method to call any others in a generic manner. These methods are exposed on pnp.sp.utility and support batching and caching.

    +

    sendEmail

    +

    This methods allows you to send an email based on the supplied arguments. The method takes a single argument, a plain object defined by the EmailProperties interface (shown below).

    +

    EmailProperties

    +
    export interface EmailProperties {
    +
    +    To: string[];
    +    CC?: string[];
    +    BCC?: string[];
    +    Subject: string;
    +    Body: string;
    +    AdditionalHeaders?: TypedHash<string>;
    +    From?: string;
    +}
    +
    +

    Usage

    +

    You must define the To, Subject, and Body values - the remaining are optional.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +import { IEmailProperties } from "@pnp/sp/sputilities";
    +
    +const emailProps: IEmailProperties = {
    +    To: ["user@site.com"],
    +    CC: ["user2@site.com", "user3@site.com"],
    +    BCC: ["user4@site.com", "user5@site.com"],
    +    Subject: "This email is about...",
    +    Body: "Here is the body. <b>It supports html</b>",
    +    AdditionalHeaders: {
    +        "content-type": "text/html"
    +    }
    +};
    +
    +await sp.utility.sendEmail(emailProps);
    +console.log("Email Sent!");
    +
    +

    getCurrentUserEmailAddresses

    +

    This method returns the current user's email addresses known to SharePoint.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +
    +let addressString: string = await sp.utility.getCurrentUserEmailAddresses();
    +
    +// and use it with sendEmail
    +await sp.utility.sendEmail({
    +    To: [addressString],
    +    Subject: "This email is about...",
    +    Body: "Here is the body. <b>It supports html</b>",
    +    AdditionalHeaders: {
    +        "content-type": "text/html"
    +    },
    +});
    +
    +

    resolvePrincipal

    +

    Gets information about a principal that matches the specified Search criteria

    +
    import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +
    +let principal : IPrincipalInfo = await sp.utility.resolvePrincipal("user@site.com", PrincipalType.User, PrincipalSource.All, true, false, true);
    +
    +console.log(principal);
    +
    +

    searchPrincipals

    +

    Gets information about the principals that match the specified Search criteria.

    +
    import { sp, IPrincipalInfo, PrincipalType, PrincipalSource } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +
    +let principals : IPrincipalInfo[] = await sp.utility.searchPrincipals("john", PrincipalType.User, PrincipalSource.All,"", 10);
    +
    +console.log(principals);
    +
    +

    createEmailBodyForInvitation

    +

    Gets the external (outside the firewall) URL to a document or resource in a site.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +
    +let url : string = await sp.utility.createEmailBodyForInvitation("https://contoso.sharepoint.com/sites/dev/SitePages/DevHome.aspx");
    +console.log(url);
    +
    +

    expandGroupsToPrincipals

    +

    Resolves the principals contained within the supplied groups

    +
    import { sp, IPrincipalInfo } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +
    +let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"]);
    +console.log(principals);
    +
    +// optionally supply a max results count. Default is 30.
    +let principals : IPrincipalInfo[] = await sp.utility.expandGroupsToPrincipals(["Dev Owners", "Dev Members"], 10);
    +console.log(principals);
    +
    +

    createWikiPage

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/sputilities";
    +import { ICreateWikiPageResult } from "@pnp/sp/sputilities";
    +
    +let newPage : ICreateWikiPageResult = await sp.utility.createWikiPage({
    +    ServerRelativeUrl: "/sites/dev/SitePages/mynewpage.aspx",
    +    WikiHtmlContent: "This is my <b>page</b> content. It supports rich html.",
    +});
    +
    +// newPage contains the raw data returned by the service
    +console.log(newPage.data);
    +
    +// newPage contains a File instance you can use to further update the new page
    +let file = await newPage.file();
    +console.log(file);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/subscriptions/index.html b/v2/sp/subscriptions/index.html new file mode 100644 index 000000000..daac7a95b --- /dev/null +++ b/v2/sp/subscriptions/index.html @@ -0,0 +1,2395 @@ + + + + + + + + + + + + + + + + + + Subscriptions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/subscriptions

    +

    Webhooks on a SharePoint list are used to notify any change in the list, to other applications using a push model. This module provides methods to add, update or delete webhooks on a particular SharePoint list or library.

    +

    ISubscriptions

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/lists";
    import { Subscriptions, ISubscriptions} from "@pnp/sp/subscriptions";
    import "@pnp/sp/subscriptions/list"
    Preset: Allimport {sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription} from "@pnp/sp/presets/all";
    +

    Add a webhook

    +

    Using this library, you can add a webhook to a specified list within the SharePoint site.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +
    +import { Subscriptions, ISubscriptions} from "@pnp/sp/subscriptions";
    +import "@pnp/sp/subscriptions/list";
    +
    +// This is the URL which will be called by SharePoint when there is a change in the list
    +const notificationUrl = "<notification-url>";
    +
    +// Set the expiry date to 180 days from now, which is the maximum allowed for the webhook expiry date.
    +const expiryDate = dateAdd(new Date(), "day" , 180).toISOString();
    +
    +// Adds a webhook to the Documents library
    +var res = await sp.web.lists.getByTitle("Documents").subscriptions.add(notificationUrl,expiryDate);
    +
    +

    Get all webhooks added to a list

    +

    Read all the webhooks' details which are associated to the list

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/subscriptions";
    +
    +const res = await sp.web.lists.getByTitle("Documents").subscriptions();
    +
    +

    ISubscription

    +

    This interface provides the methods for managing a particular webhook.

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selectiveimport { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    import "@pnp/sp/lists";
    import { Subscriptions, ISubscriptions, Subscription, ISubscription} from "@pnp/sp/subscriptions";
    import "@pnp/sp/subscriptions/list"
    Preset: Allimport { sp, Webs, IWebs, Lists, ILists, Subscriptions, ISubscriptions, Subscription, ISubscription } from "@pnp/sp/presets/all";
    +

    Managing a webhook

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/subscriptions";
    +
    +// Get details of a webhook based on its ID
    +const webhookId = "1f029e5c-16e4-4941-b46f-67895118763f";
    +const webhook = await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId)();
    +
    +// Update a webhook
    +const newDate = dateAdd(new Date(), "day" , 150).toISOString();
    +const updatedWebhook = await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId).update(newDate);
    +
    +// Delete a webhook
    +await sp.web.lists.getByTitle("Documents").subscriptions.getById(webhookId).delete();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/taxonomy/index.html b/v2/sp/taxonomy/index.html new file mode 100644 index 000000000..72cd16f12 --- /dev/null +++ b/v2/sp/taxonomy/index.html @@ -0,0 +1,2565 @@ + + + + + + + + + + + + + + + + + + Taxonomy - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/taxonomy

    +

    Provides access to the v2.1 api term store

    +

    Docs updated with v2.0.9 release as the underlying API changed.

    +
    +

    NOTE: This API may change so please be aware updates to the taxonomy module will not trigger a major version bump in PnPjs even if they are breaking. Once things stabalize this note will be removed.

    +
    +

    Invokable Banner Selective Imports Banner

    +

    Term Store

    +

    Access term store data from the root sp object as shown below.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermStoreInfo } from "@pnp/sp/taxonomy";
    +
    +// get term store data
    +const info: ITermStoreInfo = await sp.termStore();
    +
    +

    Term Groups

    +

    Access term group information

    +

    List

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermGroupInfo } from "@pnp/sp/taxonomy";
    +
    +// get term groups
    +const info: ITermGroupInfo[] = await sp.termStore.groups();
    +
    +

    Get By Id

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermGroupInfo } from "@pnp/sp/taxonomy";
    +
    +// get term groups data
    +const info: ITermGroupInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72")();
    +
    +

    Term Sets

    +

    Access term set information

    +

    List

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermSetInfo } from "@pnp/sp/taxonomy";
    +
    +// get get set info
    +const info: ITermSetInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets();
    +
    +

    Get By Id

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermSetInfo } from "@pnp/sp/taxonomy";
    +
    +// get term set data
    +const info: ITermSetInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72")();
    +
    +

    getAllChildrenAsOrderedTree

    +

    Added in 2.0.13

    +

    This method will get all of a set's child terms in an ordered array. It is a costly method in terms of requests so we suggest you cache the results as taxonomy trees seldom change.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermInfo } from "@pnp/sp/taxonomy";
    +import { dateAdd, PnPClientStorage } from "@pnp/core";
    +
    +// here we get all the children of a given set
    +const childTree = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getAllChildrenAsOrderedTree();
    +
    +// here we show caching the results using the PnPClientStorage class, there are many caching libraries and options available
    +const store = new PnPClientStorage();
    +
    +// our tree likely doesn't change much in 30 minutes for most applications
    +// adjust to be longer or shorter as needed
    +const cachedTree = await store.local.getOrPut("myKey", () => {
    +    return sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getAllChildrenAsOrderedTree();
    +}, dateAdd(new Date(), "minute", 30));
    +
    +

    Terms

    +

    Access term set information

    +

    List

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermInfo } from "@pnp/sp/taxonomy";
    +
    +// list all the terms that are direct children of this set
    +const infos: ITermInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").children();
    +
    +

    List (terms)

    +

    Added in 2.0.13

    +

    You can use the terms property to get a flat list of all terms in the set. These terms do not contain parent/child relationship information.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermInfo } from "@pnp/sp/taxonomy";
    +
    +// list all the terms that are direct children of this set
    +const infos: ITermInfo[] = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").terms();
    +
    +

    Get By Id

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +import { ITermInfo } from "@pnp/sp/taxonomy";
    +
    +// get term set data
    +const info: ITermInfo = await sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72").getTermById("338666a8-1111-2222-3333-f72471314e72")();
    +
    +

    Get Term Parent

    +

    Behavior Change in 2.1.0

    +

    The server API changed again, resulting in the removal of the "parent" property from ITerm as it is not longer supported as a path property. You now must use "expand" to load a term's parent information. The side affect of this is that the parent is no longer chainable, meaning you need to load a new term instance to work with the parent term. An approach for this is shown below.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/taxonomy";
    +
    +// get a ref to the set
    +const set = sp.termStore.groups.getById("338666a8-1111-2222-3333-f72471314e72").sets.getById("338666a8-1111-2222-3333-f72471314e72");
    +
    +// get a term's information and expand parent to get the parent info as well
    +const w = await set.getTermById("338666a8-1111-2222-3333-f72471314e72").expand("parent")();
    +
    +// get a ref to the parent term
    +const parent = set.getTermById(w.parent.id);
    +
    +// make a request for the parent term's info - this data currently match the results in the expand call above, but this
    +// is to demonstrate how to gain a ref to the parent and select its data
    +const parentInfo = await parent.select("Id", "Descriptions")();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/tenant-properties/index.html b/v2/sp/tenant-properties/index.html new file mode 100644 index 000000000..d213d2fa8 --- /dev/null +++ b/v2/sp/tenant-properties/index.html @@ -0,0 +1,2286 @@ + + + + + + + + + + + + + + + + + + Tenant Properties - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/web - tenant properties

    +

    You can set, read, and remove tenant properties using the methods shown below:

    +

    setStorageEntity

    +

    This method MUST be called in the context of the app catalog web or you will get an access denied message.

    +
    import { Web } from "@pnp/sp/webs";
    +
    +const w = Web("https://tenant.sharepoint.com/sites/appcatalog/");
    +
    +// specify required key and value
    +await w.setStorageEntity("Test1", "Value 1");
    +
    +// specify optional description and comments
    +await w.setStorageEntity("Test2", "Value 2", "description", "comments");
    +
    +

    getStorageEntity

    +

    This method can be used from any web to retrieve values previously set.

    +
    import { sp, IStorageEntity } from "@pnp/sp/presets/all";
    +
    +const prop: IStorageEntity = await sp.web.getStorageEntity("Test1");
    +
    +console.log(prop.Value);
    +
    +

    removeStorageEntity

    +

    This method MUST be called in the context of the app catalog web or you will get an access denied message.

    +
    import { Web } from "@pnp/sp/webs";
    +
    +const w = Web("https://tenant.sharepoint.com/sites/appcatalog/");
    +
    +await w.removeStorageEntity("Test1");
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/user-custom-actions/index.html b/v2/sp/user-custom-actions/index.html new file mode 100644 index 000000000..6d622a78f --- /dev/null +++ b/v2/sp/user-custom-actions/index.html @@ -0,0 +1,2424 @@ + + + + + + + + + + + + + + + + + + User custom actions - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    @pnp/sp/user-custom-actions

    +

    Represents a custom action associated with a SharePoint list, web or site collection.

    +

    IUserCustomActions

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { IUserCustomActions, IUserCustomAction } from "@pnp/sp/user-custom-actions";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/user-custom-actions";
    Preset: Allimport { sp, IUserCustomActions, IUserCustomAction } from "@pnp/sp/presents/all";
    +

    Get a collection of User Custom Actions from a web

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/user-custom-actions";
    +
    +const userCustomActions = sp.web.userCustomActions();
    +
    +

    Add a new User Custom Action

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/user-custom-actions";
    +import { IUserCustomActionAddResult } from '@pnp/sp/user-custom-actions';
    +
    +const newValues: TypedHash<string> = {
    +    "Title": "New Title",
    +    "Description": "New Description",
    +    "Location": "ScriptLink",
    +    "ScriptSrc": "https://..."
    +};
    +
    +const response : IUserCustomActionAddResult = await sp.web.userCustomActions.add(newValues);
    +
    +

    Get a User Custom Action by ID

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/user-custom-actions";
    +
    +const uca: IUserCustomAction = sp.web.userCustomActions.getById("00000000-0000-0000-0000-000000000000");
    +
    +const ucaData = await uca();
    +
    +

    Clear the User Custom Action collection

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/user-custom-actions";
    +
    +// Site collection level
    +await sp.site.userCustomActions.clear();
    +
    +// Site (web) level
    +await sp.web.userCustomActions.clear();
    +
    +// List level
    +await sp.web.lists.getByTitle("Documents").userCustomActions.clear();
    +
    +

    IUserCustomAction

    +

    Update an existing User Custom Action

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/user-custom-actions";
    +import { IUserCustomActionUpdateResult } from '@pnp/sp/user-custom-actions';
    +
    +const uca = sp.web.userCustomActions.getById("00000000-0000-0000-0000-000000000000");
    +
    +const newValues: TypedHash<string> = {
    +    "Title": "New Title",
    +    "Description": "New Description",
    +    "ScriptSrc": "https://..."
    +};
    +
    +const response: IUserCustomActionUpdateResult = uca.update(newValues);
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/views/index.html b/v2/sp/views/index.html new file mode 100644 index 000000000..590d433ff --- /dev/null +++ b/v2/sp/views/index.html @@ -0,0 +1,2652 @@ + + + + + + + + + + + + + + + + + + Views - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/views

    +

    Views define the columns, ordering, and other details we see when we look at a list. You can have multiple views for a list, including private views - and one default view.

    +

    IViews

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Views, IViews } from "@pnp/sp/views";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/views";
    Preset: Allimport { sp, Views, IViews } from "@pnp/sp/presets/all";
    +

    Get views in a list

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const list = sp.web.lists.getByTitle("My List");
    +
    +// get all the views and their properties
    +const views1 = await list.views();
    +
    +// you can use odata select operations to get just a set a fields
    +const views2 = await list.views.select("Id", "Title")();
    +
    +// get the top three views
    +const views3 = await list.views.top(3)();
    +
    +

    Add a View

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const list = sp.web.lists.getByTitle("My List");
    +
    +// create a new view with default fields and properties
    +const result = await list.views.add("My New View");
    +
    +// create a new view with specific properties
    +const result2 = await list.views.add("My New View 2", false, {
    +    RowLimit: 10,
    +    ViewQuery: "<OrderBy><FieldRef Name='Modified' Ascending='False' /></OrderBy>",
    +});
    +
    +// manipulate the view's fields
    +await result2.view.fields.removeAll();
    +
    +await Promise.all([
    +    result2.view.fields.add("Title"),
    +    result2.view.fields.add("Modified"),
    +]);
    +
    +

    IView

    +

    Get a View's Information

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const list = sp.web.lists.getByTitle("My List");
    +
    +const result = await list.views.getById("{GUID view id}")();
    +
    +const result2 = await list.views.getByTitle("My View")();
    +
    +const result3 = await list.views.getByTitle("My View").select("Id", "Title")();
    +
    +const result4 = await list.defaultView();
    +
    +const result5 = await list.getView("{GUID view id}")();
    +
    +

    fields

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const list = sp.web.lists.getByTitle("My List");
    +
    +const result = await list.views.getById("{GUID view id}").fields();
    +
    +

    update

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const list = sp.web.lists.getByTitle("My List");
    +
    +const result = await list.views.getById("{GUID view id}").update({
    +    RowLimit: 20,
    +});
    +
    +

    renderAsHtml

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const result = await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").renderAsHtml();
    +
    +

    setViewXml

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const viewXml = "...";
    +
    +await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").setViewXml(viewXml);
    +
    +

    delete

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const viewXml = "...";
    +
    +await sp.web.lists.getByTitle("My List").views.getById("{GUID view id}").delete();
    +
    +

    ViewFields

    +

    getSchemaXml

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +const xml = await sp.web.lists.getByTitle("My List").defaultView.fields.getSchemaXml();
    +
    +

    add

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +await sp.web.lists.getByTitle("My List").defaultView.fields.add("Created");
    +
    +

    move

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +await sp.web.lists.getByTitle("My List").defaultView.fields.move("Created", 0);
    +
    +

    remove

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +await sp.web.lists.getByTitle("My List").defaultView.fields.remove("Created");
    +
    +

    removeAll

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/lists";
    +import "@pnp/sp/views";
    +
    +await sp.web.lists.getByTitle("My List").defaultView.fields.removeAll();
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/sp/webs/index.html b/v2/sp/webs/index.html new file mode 100644 index 000000000..029248cf6 --- /dev/null +++ b/v2/sp/webs/index.html @@ -0,0 +1,4190 @@ + + + + + + + + + + + + + + + + + + Webs - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    + + + + + + + +

    @pnp/sp/webs

    +

    Webs are one of the fundamental entry points when working with SharePoint. Webs serve as a container for lists, features, sub-webs, and all of the entity types.

    +

    IWebs

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Webs, IWebs } from "@pnp/sp/webs";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    Preset: Allimport { sp, Webs, IWebs } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp, Webs, IWebs } from "@pnp/sp/presets/core";
    +

    Add Web

    +

    Using the library you can add a web to another web's collection of subwebs. The simplest usage requires only a title and url. This will result in a team site with all of the default settings. You can also provide other settings such as description, template, language, and inherit permissions.

    +
    import { sp } from "@pnp/sp";
    +import { IWebAddResult } from "@pnp/sp/webs";
    +
    +const result = await sp.web.webs.add("title", "subweb1");
    +
    +// show the response from the server when adding the web
    +console.log(result.data);
    +
    +// we can immediately operate on the new web
    +result.web.select("Title")().then((w: IWebAddResult)  => {
    +
    +    // show our title
    +    console.log(w.Title);
    +});
    +
    +
    import { sp } from "@pnp/sp";
    +import { IWebAddResult } from "@pnp/sp/webs";
    +
    +// create a German language wiki site with title, url, description, which does not inherit permissions
    +sp.web.webs.add("wiki", "subweb2", "a wiki web", "WIKI#0", 1031, false).then((w: IWebAddResult) => {
    +
    +  // ...
    +});
    +
    +

    IWeb

    +

    Invokable Banner Selective Imports Banner

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import { sp } from "@pnp/sp";
    import { Web, IWeb } from "@pnp/sp/webs";
    Selective 2import { sp } from "@pnp/sp";
    import "@pnp/sp/webs";
    Preset: Allimport { sp, Web, IWeb } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp, Web, IWeb } from "@pnp/sp/presets/core";
    +

    Access a Web

    +

    There are several ways to access a web instance, each of these methods is equivalent in that you will have an IWeb instance to work with. All of the examples below use a variable named "web" which represents an IWeb instance - regardless of how it was initially accessed.

    +

    Access the web from the imported "sp" object using selective import:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +const r = await sp.web();
    +
    +

    Access the web from the imported "sp" using the 'all' preset

    +
    import { sp } from "@pnp/sp/presets/all";
    +
    +const r = await sp.web();
    +
    +

    Access the web from the imported "sp" using the 'core' preset

    +
    import { sp } from "@pnp/sp/presets/core";
    +
    +const r = await sp.web();
    +
    +

    Create a web instance using the factory function

    +
    import { Web } from "@pnp/sp/webs";
    +
    +const web = Web("https://something.sharepoint.com/sites/dev");
    +const r = await web();
    +
    +

    webs

    +

    Access the child webs collection of this web

    +
    const webs = web.webs();
    +
    +

    Get A Web's properties

    +
    // basic get of the webs properties
    +const props = await web();
    +
    +// use odata operators to get specific fields
    +const props2 = await web.select("Title")();
    +
    +// type the result to match what you are requesting
    +const props3 = await web.select("Title")<{ Title: string }>();
    +
    +

    getParentWeb

    +

    Get the data and IWeb instance for the parent web for the given web instance

    +
    import { IOpenWebByIdResult } from "@pnp/sp/sites";
    +const web: IOpenWebByIdResult = web.getParentWeb();
    +
    +

    getSubwebsFilteredForCurrentUser

    +

    Returns a collection of objects that contain metadata about subsites of the current site in which the current user is a member.

    +
    const subWebs = await web.getSubwebsFilteredForCurrentUser()();
    +
    +// apply odata operations to the collection
    +const subWebs2 = await sp.web.getSubwebsFilteredForCurrentUser().select("Title", "Language").orderBy("Created", true)();
    +
    +
    +

    Note: getSubwebsFilteredForCurrentUser returns IWebInfosData which is a subset of all the available fields on IWebInfo.

    +
    +

    allProperties

    +

    Allows access to the web's all properties collection. This is readonly in REST.

    +
    const props = await web.allProperties();
    +
    +// select certain props
    +const props2 = await web.allProperties.select("prop1", "prop2")();
    +
    +

    webinfos

    +

    Gets a collection of WebInfos for this web's subwebs

    +
    const infos = await web.webinfos();
    +
    +// or select certain fields
    +const infos2 = await web.webinfos.select("Title", "Description")();
    +
    +// or filter
    +const infos3 = await web.webinfos.filter("Title eq 'MyWebTitle'")();
    +
    +// or both
    +const infos4 = await web.webinfos.select("Title", "Description").filter("Title eq 'MyWebTitle'")();
    +
    +// get the top 4 ordered by Title
    +const infos5 = await web.webinfos.top(4).orderBy("Title")();
    +
    +
    +

    Note: webinfos returns IWebInfosData which is a subset of all the available fields on IWebInfo.

    +
    +

    update

    +

    Updates this web instance with the supplied properties

    +
    
    +// update the web's title and description
    +const result = await web.update({
    +    Title: "New Title",
    +    Description: "My new description",
    +});
    +
    +// a project implementation could wrap the update to provide type information for your expected fields:
    +import { IWebUpdateResult } from "@pnp/sp/webs";
    +
    +interface IWebUpdateProps {
    +    Title: string;
    +    Description: string;
    +}
    +
    +function updateWeb(props: IWebUpdateProps): Promise<IWebUpdateResult> {
    +    web.update(props);
    +}
    +
    +

    Delete a Web

    +
    await web.delete();
    +
    +

    applyTheme

    +

    Applies the theme specified by the contents of each of the files specified in the arguments to the site

    +
    import { combine } from "@pnp/core";
    +
    +// we are going to apply the theme to this sub web as an example
    +const web = Web("https://{tenant}.sharepoint.com/sites/dev/subweb");
    +
    +// the urls to the color and font need to both be from the catalog at the root
    +// these urls can be constants or calculated from existing urls
    +const colorUrl =  combine("/", "sites/dev", "_catalogs/theme/15/palette011.spcolor");
    +// this gives us the same result
    +const fontUrl = "/sites/dev/_catalogs/theme/15/fontscheme007.spfont";
    +
    +// apply the font and color, no background image, and don't share this theme
    +await web.applyTheme(colorUrl, fontUrl, "", false);
    +
    +

    applyWebTemplate & availableWebTemplates

    +

    Applies the specified site definition or site template to the Web site that has no template applied to it. This is seldom used outside provisioning scenarios.

    +
    const templates = (await web.availableWebTemplates().select("Name")<{ Name: string }[]>()).filter(t => /ENTERWIKI#0/i.test(t.Name));
    +
    +// apply the wiki template
    +const template = templates.length > 0 ? templates[0].Name : "STS#0";
    +
    +await web.applyWebTemplate(template);
    +
    +

    getChanges

    +

    Returns the collection of changes from the change log that have occurred within the web, based on the specified query.

    +
    // get the web changes including add, update, and delete
    +const changes = await web.getChanges({
    +        Add: true,
    +        ChangeTokenEnd: null,
    +        ChangeTokenStart: null,
    +        DeleteObject: true,
    +        Update: true,
    +        Web: true,
    +    });
    +
    +

    mapToIcon

    +

    Returns the name of the image file for the icon that is used to represent the specified file

    +
    import { combine } from "@pnp/core";
    +
    +const iconFileName = await web.mapToIcon("test.docx");
    +// iconPath === "icdocx.png"
    +// which you can need to map to a real url
    +const iconFullPath = `https://{tenant}.sharepoint.com/sites/dev/_layouts/images/${iconFileName}`;
    +
    +// OR dynamically
    +const webData = await sp.web.select("Url")();
    +const iconFullPath2 = combine(webData.Url, "_layouts", "images", iconFileName);
    +
    +// OR within SPFx using the context
    +const iconFullPath3 = combine(this.context.pageContext.web.absoluteUrl, "_layouts", "images", iconFileName);
    +
    +// You can also set size
    +// 16x16 pixels = 0, 32x32 pixels = 1
    +const icon32FileName = await web.mapToIcon("test.docx", 1);
    +
    +

    storage entities

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/appcatalog";
    +import { IStorageEntity } from "@pnp/sp/webs";
    +
    +// needs to be unique, GUIDs are great
    +const key = "my-storage-key";
    +
    +// read an existing entity
    +const entity: IStorageEntity = await web.getStorageEntity(key);
    +
    +// setStorageEntity and removeStorageEntity must be called in the context of the tenant app catalog site
    +// you can get the tenant app catalog using the getTenantAppCatalogWeb
    +const tenantAppCatalogWeb = await sp.getTenantAppCatalogWeb();
    +
    +tenantAppCatalogWeb.setStorageEntity(key, "new value");
    +
    +// set other properties
    +tenantAppCatalogWeb.setStorageEntity(key, "another value", "description", "comments");
    +
    +const entity2: IStorageEntity = await web.getStorageEntity(key);
    +/*
    +entity2 === {
    +    Value: "another value",
    +    Comment: "comments";
    +    Description: "description",
    +};
    +*/
    +
    +// you can also remove a storage entity
    +await tenantAppCatalogWeb.removeStorageEntity(key);
    +
    +

    appcatalog imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/appcatalog";
    Selective 2import "@pnp/sp/appcatalog/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    getAppCatalog

    +

    Returns this web as an IAppCatalog instance or creates a new IAppCatalog instance from the provided url.

    +
    import { IApp } from "@pnp/sp/appcatalog";
    +
    +const appWeb = web.getAppCatalog();
    +// appWeb url === web url
    +
    +const app: IApp = appWeb.getAppById("{your app id}");
    +
    +const appWeb2 = web.getAppCatalog("https://tenant.sharepoing.com/sites/someappcatalog");
    +// appWeb2 url === "https://tenant.sharepoing.com/sites/someappcatalog"
    +
    +

    client-side-pages imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/client-side-pages";
    Selective 2import "@pnp/sp/client-side-pages/web";
    Preset: Allimport { sp, Web, IWeb } from "@pnp/sp/presets/all";
    +

    You can create and load clientside page instances directly from a web. More details on working with clientside pages are available in the dedicated article.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +import "@pnp/sp/clientside-pages/web";
    +
    +// simplest add a page example
    +const page = await sp.web.addClientsidePage("mypage1");
    +
    +// simplest load a page example
    +const page = await sp.web.loadClientsidePage("/sites/dev/sitepages/mypage3.aspx");
    +
    +

    content-type imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/content-types";
    Selective 2import "@pnp/sp/content-types/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    contentTypes

    +

    Allows access to the collection of content types in this web.

    +
    const cts = await web.contentTypes();
    +
    +// you can also select fields and use other odata operators
    +const cts2 = await web.contentTypes.select("Name")();
    +
    +

    features imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/features";
    Selective 2import "@pnp/sp/features/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    features

    +

    Allows access to the collection of content types in this web.

    +
    const features = await web.features();
    +
    +

    fields imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/fields";
    Selective 2import "@pnp/sp/fields/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    fields

    +

    Allows access to the collection of fields in this web.

    +
    const fields = await web.fields();
    +
    +

    files imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/files";
    Selective 2import "@pnp/sp/files/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    getFileByServerRelativeUrl

    +

    Gets a file by server relative url

    +
    import { IFile } from "@pnp/sp/files";
    +
    +const file: IFile = web.getFileByServerRelativeUrl("/sites/dev/library/myfile.docx");
    +
    +

    getFileByServerRelativePath

    +

    Gets a file by server relative url if your file name contains # and % characters

    +
    import { IFile } from "@pnp/sp/files";
    +
    +const file: IFile = web.getFileByServerRelativePath("/sites/dev/library/my # file%.docx");
    +
    +

    folders imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/folders";
    Selective 2import "@pnp/sp/folders/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    folders

    +

    Gets the collection of folders in this web

    +
    const folders = await web.folders();
    +
    +// you can also filter and select as with any collection
    +const folders2 = await web.folders.select("ServerRelativeUrl", "TimeLastModified").filter("ItemCount gt 0")();
    +
    +// or get the most recently modified folder
    +const folders2 = await web.folders.orderBy("TimeLastModified").top(1)();
    +
    +

    rootFolder

    +

    Gets the root folder of the web

    +
    const folder = await web.rootFolder();
    +
    +

    getFolderByServerRelativeUrl

    +

    Gets a folder by server relative url

    +
    import { IFolder } from "@pnp/sp/folders";
    +
    +const folder: IFolder = web.getFolderByServerRelativeUrl("/sites/dev/library");
    +
    +

    getFolderByServerRelativePath

    +

    Gets a folder by server relative url if your folder name contains # and % characters

    +
    import { IFolder } from "@pnp/sp/folders";
    +
    +const folder: IFolder = web.getFolderByServerRelativePath("/sites/dev/library/my # folder%/");
    +
    +

    hubsites imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/hubsites";
    Selective 2import "@pnp/sp/hubsites/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    hubSiteData

    +

    Gets hub site data for the current web

    +
    import { IHubSiteWebData } from "@pnp/sp/hubsites";
    +
    +// get the data and force a refresh
    +const data: IHubSiteWebData = await web.hubSiteData(true);
    +
    +

    syncHubSiteTheme

    +

    Applies theme updates from the parent hub site collection

    +
    await web.syncHubSiteTheme();
    +
    +

    lists imports

    + + + + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/lists";
    Selective 2import "@pnp/sp/lists/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    Preset: Coreimport { sp } from "@pnp/sp/presets/core";
    +

    lists

    +

    Gets the collection of all lists that are contained in the Web site

    +
    import { ILists } from "@pnp/sp/lists";
    +
    +const lists: ILists = web.lists;
    +
    +// you can always order the lists and select properties
    +const data = await lists.select("Title").orderBy("Title")();
    +
    +// and use other odata operators as well
    +const data2 = await web.lists.top(3).orderBy("LastItemModifiedDate")();
    +
    +

    siteUserInfoList

    +

    Gets the UserInfo list of the site collection that contains the Web site

    +
    import { IList } from "@pnp/sp/lists";
    +
    +const list: IList = web.siteUserInfoList;
    +
    +const data = await list();
    +
    +// or chain off that list to get additional details
    +const items = await list.items.top(2)();
    +
    +

    defaultDocumentLibrary

    +

    Get a reference the default documents library of a web

    +
    import { IList } from "@pnp/sp/lists";
    +
    +const list: IList = web.defaultDocumentLibrary;
    +
    +

    customListTemplates

    +

    Gets the collection of all list definitions and list templates that are available

    +
    import { IList } from "@pnp/sp/lists";
    +
    +const templates = await web.customListTemplates();
    +
    +// odata operators chain off the collection as expected
    +const templates2 = await web.customListTemplates.select("Title")();
    +
    +

    getList

    +

    Gets a list by server relative url (list's root folder)

    +
    import { IList } from "@pnp/sp/lists";
    +
    +const list: IList = web.getList("/sites/dev/lists/test");
    +
    +const listData = list();
    +
    +

    getCatalog

    +

    Returns the list gallery on the site

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameValue
    WebTemplateCatalog111
    WebPartCatalog113
    ListTemplateCatalog114
    MasterPageCatalog116
    SolutionCatalog121
    ThemeCatalog123
    DesignCatalog124
    AppDataCatalog125
    +
    import { IList } from "@pnp/sp/lists";
    +
    +const templateCatalog: IList = await web.getCatalog(111);
    +
    +const themeCatalog: IList = await web.getCatalog(123);
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/navigation";
    Selective 2import "@pnp/sp/navigation/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    + +

    Gets a navigation object that represents navigation on the Web site, including the Quick Launch area and the top navigation bar

    +
    import { INavigation } from "@pnp/sp/navigation";
    +
    +const nav: INavigation = web.navigation;
    +
    +const navData = await nav();
    +
    +

    regional-settings imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/regional-settings";
    Selective 2import "@pnp/sp/regional-settings/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +
    import { IRegionalSettings } from "@pnp/sp/navigation";
    +
    +const settings: IRegionalSettings = web.regionalSettings;
    +
    +const settingsData = await settings();
    +
    + + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/related-items";
    Selective 2import "@pnp/sp/related-items/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +
    import { IRelatedItemManager, IRelatedItem } from "@pnp/sp/related-items";
    +
    +const manager: IRelatedItemManager = web.relatedItems;
    +
    +const data: IRelatedItem[] = await manager.getRelatedItems("{list name}", 4);
    +
    +

    security imports

    +

    Please see information around the available security methods in the security article.

    +

    sharing imports

    +

    Please see information around the available sharing methods in the sharing article.

    +

    site-groups imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/site-groups";
    Selective 2import "@pnp/sp/site-groups/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    siteGroups

    +

    The site groups

    +
    const groups = await web.siteGroups();
    +
    +const groups2 = await web.siteGroups.top(2)();
    +
    +

    associatedOwnerGroup

    +

    The web's owner group

    +
    const group = await web.associatedOwnerGroup();
    +
    +const users = await web.associatedOwnerGroup.users();
    +
    +

    associatedMemberGroup

    +

    The web's member group

    +
    const group = await web.associatedMemberGroup();
    +
    +const users = await web.associatedMemberGroup.users();
    +
    +

    associatedVisitorGroup

    +

    The web's visitor group

    +
    const group = await web.associatedVisitorGroup();
    +
    +const users = await web.associatedVisitorGroup.users();
    +
    +

    createDefaultAssociatedGroups

    +

    Creates the default associated groups (Members, Owners, Visitors) and gives them the default permissions on the site. The target site must have unique permissions and no associated members / owners / visitors groups

    +
    await web.createDefaultAssociatedGroups("Contoso", "{first owner login}");
    +
    +// copy the role assignments
    +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}", true);
    +
    +// don't clear sub assignments
    +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, false);
    +
    +// specify secondary owner, don't copy permissions, clear sub scopes
    +await web.createDefaultAssociatedGroups("Contoso", "{first owner login}", false, true, "{second owner login}");
    +
    +

    site-users imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/site-users";
    Selective 2import "@pnp/sp/site-users/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    siteUsers

    +

    The site users

    +
    const users = await web.siteUsers();
    +
    +const users2 = await web.siteUsers.top(5)();
    +
    +const users3 = await web.siteUsers.filter(`startswith(LoginName, '${encodeURIComponent("i:0#.f|m")}')`)();
    +
    +

    currentUser

    +

    Information on the current user

    +
    const user = await web.currentUser();
    +
    +// check the login name of the current user
    +const user2 = await web.currentUser.select("LoginName")();
    +
    +

    ensureUser

    +

    Checks whether the specified login name belongs to a valid user in the web. If the user doesn't exist, adds the user to the web

    +
    import { IWebEnsureUserResult } from "@pnp/sp/site-users/";
    +
    +const result: IWebEnsureUserResult = await web.ensureUser("i:0#.f|membership|user@domain.onmicrosoft.com");
    +
    +

    getUserById

    +

    Returns the user corresponding to the specified member identifier for the current web

    +
    import { ISiteUser } from "@pnp/sp/site-users/";
    +
    +const user: ISiteUser = web.getUserById(23);
    +
    +const userData = await user();
    +
    +const userData2 = await user.select("LoginName")();
    +
    +

    user-custom-actions imports

    + + + + + + + + + + + + + + + + + + + + + +
    ScenarioImport Statement
    Selective 1import "@pnp/sp/user-custom-actions";
    Selective 2import "@pnp/sp/user-custom-actions/web";
    Preset: Allimport { sp } from "@pnp/sp/presets/all";
    +

    userCustomActions

    +

    Gets a newly refreshed collection of the SPWeb's SPUserCustomActionCollection

    +
    import { IUserCustomActions } from "@pnp/sp/user-custom-actions";
    +
    +const actions: IUserCustomActions = web.userCustomActions;
    +
    +const actionsData = await actions();
    +
    +

    IWebInfosData

    +

    Some web operations return a subset of web information defined by the IWebInfosData interface, shown below. In those cases only these fields are available for select, orderby, and other odata operations.

    +
    interface IWebInfosData {
    +    Configuration: number;
    +    Created: string;
    +    Description: string;
    +    Id: string;
    +    Language: number;
    +    LastItemModifiedDate: string;
    +    LastItemUserModifiedDate: string;
    +    ServerRelativeUrl: string;
    +    Title: string;
    +    WebTemplate: string;
    +    WebTemplateId: number;
    +}
    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file diff --git a/v2/transition-guide/index.html b/v2/transition-guide/index.html new file mode 100644 index 000000000..d51768093 --- /dev/null +++ b/v2/transition-guide/index.html @@ -0,0 +1,2435 @@ + + + + + + + + + + + + + + + + + + Transition Guide - PnP/PnPjs + + + + + + + + + + + + + + + + + + + + + + + + + + + + + spacer + + + + + + + + + + + + + + + + + +
    + +
    + +
    + +
    + +
    + + + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + + + +
    +
    + + + + + + + +

    Transition Guide

    +

    We have worked to make moving from @pnp library 1. to 2. as painless as possible, however there are some changes to how things work. The below guide we have provided an overview of what it takes to transition between the libraries. If we missed something, please let us know in the issues list so we can update the guide. Thanks!

    +

    Installing @pnp libraries

    +

    In version 1.* the libraries were setup as peer dependencies of each other requiring you to install each of them separately. We continue to believe this correctly describes the relationship, but recognize that basically nothing in the world accounts for peer dependencies. So we have updated the libraries to be dependencies. This makes it easier to install into your projects as you only need to install a single library:

    +

    npm i --save @pnp/sp

    +

    Selective Imports

    +

    Another big change in v2 is the ability to selectively import the pieces you need from the libraries. This allows you to have smaller bundles and works well with tree-shaking. It does require you to have more import statements, which can potentially be a bit confusing at first. The selective imports apply to the sp and graph libraries.

    +

    To help explain let's take the example of the Web object. In v1 Web includes a reference to pretty much everything else in the entire sp library. Meaning that if you use web (and you pretty much have to) you hold a ref to all the other pieces (like Fields, Lists, ContentTypes) even if you aren't using them. Because of that tree shaking can't do anything to reduce the bundle size because it "thinks" you are using them simply because they have been imported. To solve this in v2 the Web object no longer contains references to anything, it is a bare object with a few methods. If you look at the source you will see that, for example, there is no longer a "lists" property. These properties and methods are now added through selectively importing the functionality you need:

    +

    Selectively Import Web lists functionality

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +// this imports the functionality for lists associated only with web
    +import "@pnp/sp/lists/web";
    +
    +const r = await sp.web.lists();
    +
    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +// this imports all the functionality for lists
    +import "@pnp/sp/lists";
    +
    +const r = await sp.web.lists();
    +
    +

    Each of the docs pages shows the selective import paths for each sub-module (lists, items, etc.).

    +

    Presets

    +

    In addition to the ability to selectively import functionality you can import presets. This allows you to import an entire set of functionality in a single line. At launch the sp library will support two presets "all" and "core" with the graph library supporting "all". Using the "all" preset will match the functionality of v1. This can save you time in transitioning your projects so you can update to selective imports later. For new projects we recommend using the selective imports from day 1.

    +

    To update your V1 projects to V2 you can replace all instances of "@pnp/sp" with "@pnp/sp/presets/all" and things should work as before (though some class names or other things may have changed, please review the change log and the rest of this guide).

    +
    // V1 way of doing things:
    +import {
    +    sp,
    +    ClientSideWebpart,
    +    ClientSideWebpartPropertyTypes,
    +} from "@pnp/sp";
    +
    +// V2 way with selective imports
    +import { sp } from "@pnp/sp";
    +import { ClientSideWebpart, ClientSideWebpartPropertyTypes } from "@pnp/sp/clientside-pages";
    +
    +// V2 way with preset "all"
    +import { sp, ClientSideWebpart, ClientSideWebpartPropertyTypes } from "@pnp/sp/presets/all";
    +
    +

    Invokable Objects

    +

    Another new feature is the addition of invokable objects. Previously where you used "get()" to invoke a request you can now leave it off. We have left the .get method in place so everyone's code wasn't broken immediately upon transitioning.

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// old way (still works)
    +const r1 = sp.web();
    +
    +// invokable
    +const r2 = sp.web();
    +
    +

    The benefit is that objects can now support default actions that are not "get" but might be "post". And you save typing a few extra characters. This still work the same as with select or any of the other odata methods:

    +
    import { sp } from "@pnp/sp";
    +import "@pnp/sp/webs";
    +
    +// invokable
    +const r = sp.web.select("Title", "Url")();
    +
    +

    Factory Functions & Interfaces

    +

    Another change in the library is in the structure of exports. We are no longer exporting the objects themselves, rather we are only exposing factory functions and interfaces. This allows us to decouple what developers use from our internal implementation. For folks using the fluent chain starting with sp you shouldn't need to update your code. If you are using any of the v1 classes directly you should just need to remove the "new" keyword and update the import path. The factory functions signature matches the constructor signature of the v1 objects.

    +
    // v1
    +import { Web } from "@pnp/sp";
    +
    +const web: Web = new Web("some absolute url");
    +
    +const r1 = web();
    +
    +// v2
    +import { Web, IWeb } from "@pnp/sp/webs";
    +
    +const web: IWeb = Web("some absolute url");
    +
    +const r2 = web();
    +
    +

    Extension Methods

    +

    Another new capability in v2 is the ability to extend objects and factories. This allows you to easily add methods or properties on a per-object basis. Please see the full article on extension methods describing this great new capability.

    +

    CDN publishing

    +

    Starting with v2 we will no longer create bundles for each of the packages. Historically these are not commonly used, don't work perfectly for everyone (there are a lot of ways to bundle things), and another piece we need to maintain. Instead we encourage folks to create their own bundles, optimized for their particular scenario. This will result in smaller overall bundle size and allow folks to bundle things to match their scenario. Please review the article on creating your custom bundles to see how to tailor bundles to your needs.

    +

    The PnPjs bundle will remain, though it is designed only for backwards compatibility and we strongly recommend creating your own bundles, or directly importing the libraries into your projects using selective imports.

    +

    Drop client-svc and sp-taxonomy libraries

    +

    These libraries were created to allow folks to access and manage SharePoint taxonomy and manage metadata. Given that there is upcoming support for taxonomy via a supported REST API we will drop these two libraries. If working with taxonomy remains a core requirement of your application and we do not yet have support for the new apis, please remain on v1 for the time being.

    +
    +

    As of 2.0.6 we support reading the modern taxonomy API. Docs here

    +
    + + + + + + + +
    +
    +
    +
    + + + + +
    + + + + + + + + + \ No newline at end of file